import {
  abortError,
  isAbortError,
  throwIfAborted
} from "./chunk-PVGDJXVK.js";
import {
  launchProcess
} from "./chunk-OR64ZGRZ.js";
import {
  getCachedQmdSearch,
  setCachedQmdSearch
} from "./chunk-PFV5C235.js";
import {
  mergeEnv
} from "./chunk-MARWOCVP.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/qmd.ts
import { createHash } from "crypto";
import os from "os";
import path from "path";
var QMD_TIMEOUT_MS = 3e4;
var QMD_DAEMON_TIMEOUT_MS = 8e3;
var QMD_PROBE_TIMEOUT_MS = 8e3;
var QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1e3;
var QMD_EMBED_BACKOFF_MS = 60 * 60 * 1e3;
var QMD_CLI_WARN_THROTTLE_MS = 15 * 60 * 1e3;
var QMD_FALLBACK_PATHS = [
  path.join(os.homedir(), ".bun", "bin", "qmd"),
  "/usr/local/bin/qmd",
  "/opt/homebrew/bin/qmd"
];
var QMD_GLOBAL_STATE_KEY = "__openclawEngramQmdGlobalState";
function getGlobalQmdState() {
  const g = globalThis;
  if (!g[QMD_GLOBAL_STATE_KEY]) {
    g[QMD_GLOBAL_STATE_KEY] = {
      warnedGlobalUpdateBehavior: false,
      lastGlobalUpdateRunAtMs: null,
      lastGlobalUpdateFailAtMs: null,
      lastGlobalEmbedRunAtMs: null,
      lastGlobalEmbedFailAtMs: null,
      lastCliWarnAtMs: null,
      lastUpdateByCollectionMs: {},
      lastUpdateFailByCollectionMs: {},
      lastEmbedByCollectionMs: {},
      lastEmbedFailByCollectionMs: {}
    };
  }
  return g[QMD_GLOBAL_STATE_KEY];
}
function errorMessage(err) {
  if (typeof err === "string") return err;
  if (err instanceof Error) return err.message;
  if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
    return err.message;
  }
  return String(err);
}
function isCallerCancellation(err, signal) {
  if (signal?.aborted) return true;
  if (isAbortError(err)) return true;
  if (err && typeof err === "object") {
    const code = "code" in err ? err.code : void 0;
    if (code === "ABORT_ERR" || code === "ERR_CANCELED") return true;
  }
  return false;
}
function isDaemonTimeoutError(err) {
  return /timed out/i.test(errorMessage(err));
}
function sleepWithSignal(ms, signal) {
  return new Promise((resolve, reject) => {
    throwIfAborted(signal);
    const timer = setTimeout(() => {
      cleanup();
      resolve();
    }, ms);
    const onAbort = () => {
      clearTimeout(timer);
      cleanup();
      reject(abortError("operation aborted while waiting"));
    };
    const cleanup = () => {
      signal?.removeEventListener("abort", onAbort);
    };
    signal?.addEventListener("abort", onAbort, { once: true });
  });
}
function isSqliteBusyError(msg) {
  const lower = msg.toLowerCase();
  return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("sqlite_busy_recovery") || lower.includes("sqliterror: database is locked");
}
function stripControlChars(s) {
  return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/[\u0000-\u001f\u007f]/g, "");
}
function truncateForLog(s, max = 2e3) {
  const cleaned = stripControlChars(s);
  return cleaned.length > max ? cleaned.slice(0, max) + "\u2026(truncated)" : cleaned;
}
function isVectorDimensionMismatchError(err) {
  const msg = err instanceof Error ? err.message : String(err);
  return /dimension mismatch/i.test(msg) || /vectors?_vec/i.test(msg) && /float\[\d+\]/i.test(msg) || /embedding/i.test(msg) && /dimensions?/i.test(msg);
}
function parseQmdVersion(version) {
  if (!version) return null;
  const match = version.match(/v?(\d{1,10})\.(\d{1,10})\.(\d{1,10})/i);
  if (!match) return null;
  return [
    Number.parseInt(match[1] ?? "0", 10),
    Number.parseInt(match[2] ?? "0", 10),
    Number.parseInt(match[3] ?? "0", 10)
  ];
}
function versionAtLeast(current, target) {
  if (!current) return false;
  for (let i = 0; i < 3; i += 1) {
    if ((current[i] ?? 0) > target[i]) return true;
    if ((current[i] ?? 0) < target[i]) return false;
  }
  return true;
}
function normalizeSearchOptions(options) {
  if (!options) return void 0;
  const intent = typeof options.intent === "string" ? options.intent.trim() : "";
  const normalized = {};
  if (intent.length > 0) {
    normalized.intent = intent;
  }
  if (options.explain === true) {
    normalized.explain = true;
  }
  return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function parseExplainScores(value) {
  if (!Array.isArray(value)) return void 0;
  const scores = value.filter((entry) => typeof entry === "number");
  return scores.length > 0 ? scores : void 0;
}
function parseQmdExplain(value) {
  if (!value || typeof value !== "object") return void 0;
  const candidate = value;
  const parsed = {
    ftsScores: parseExplainScores(candidate.ftsScores),
    vectorScores: parseExplainScores(candidate.vectorScores),
    rrf: typeof candidate.rrf === "number" ? candidate.rrf : void 0,
    rerankScore: typeof candidate.rerankScore === "number" ? candidate.rerankScore : void 0,
    blendedScore: typeof candidate.blendedScore === "number" ? candidate.blendedScore : void 0
  };
  return Object.values(parsed).some((entry) => entry !== void 0) ? parsed : void 0;
}
var AsyncMutex = class {
  locked = false;
  queue = [];
  async runExclusive(fn, signal) {
    const release = await this.acquire(signal);
    try {
      throwIfAborted(signal);
      return await fn();
    } finally {
      release();
    }
  }
  acquire(signal) {
    throwIfAborted(signal);
    if (!this.locked) {
      this.locked = true;
      return Promise.resolve(() => this.release());
    }
    return new Promise((resolve, reject) => {
      const waiter = {
        resolve: (release) => {
          signal?.removeEventListener("abort", waiter.onAbort);
          resolve(release);
        },
        reject: (reason) => {
          signal?.removeEventListener("abort", waiter.onAbort);
          reject(reason);
        },
        signal,
        onAbort: () => {
          this.queue = this.queue.filter((entry) => entry !== waiter);
          reject(abortError("operation aborted while waiting for qmd mutex"));
        }
      };
      signal?.addEventListener("abort", waiter.onAbort, { once: true });
      this.queue.push(waiter);
    });
  }
  release() {
    while (this.queue.length > 0) {
      const next = this.queue.shift();
      if (!next) break;
      if (next.signal?.aborted) {
        next.reject(abortError("operation aborted while waiting for qmd mutex"));
        continue;
      }
      this.locked = true;
      next.resolve(() => this.release());
      return;
    }
    this.locked = false;
  }
};
var QMD_MUTEX = new AsyncMutex();
function runQmd(args, timeoutMs = QMD_TIMEOUT_MS, qmdPath = "qmd", signal) {
  return QMD_MUTEX.runExclusive(async () => {
    throwIfAborted(signal, `qmd ${args.join(" ")} aborted before start`);
    const maxAttempts = isLikelyWriteCommand(args) ? 3 : 1;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await runQmdOnce(args, timeoutMs, qmdPath, signal);
      } catch (err) {
        if (isAbortError(err)) throw err;
        const msg = err instanceof Error ? err.message : String(err);
        if (attempt < maxAttempts && isSqliteBusyError(msg)) {
          await sleepWithSignal(1500 * attempt, signal);
          continue;
        }
        throw err;
      }
    }
    throw new Error("qmd command failed");
  }, signal);
}
function isLikelyWriteCommand(args) {
  const cmd = args[0] ?? "";
  return cmd === "update" || cmd === "embed" || cmd === "cleanup" || cmd === "collection";
}
function runQmdOnce(args, timeoutMs, qmdPath, signal) {
  return new Promise((resolve, reject) => {
    throwIfAborted(signal, `qmd ${args.join(" ")} aborted before spawn`);
    const child = launchProcess(qmdPath, args, {
      env: mergeEnv({ NO_COLOR: "1" }),
      stdio: ["ignore", "pipe", "pipe"]
    });
    if (!child.stdout || !child.stderr) {
      reject(new Error(`qmd ${args.join(" ")} failed to open stdio pipes`));
      return;
    }
    let stdout = "";
    let stderr = "";
    let settled = false;
    const timer = setTimeout(() => {
      settled = true;
      cleanup();
      child.kill("SIGKILL");
      reject(new Error(`qmd ${args.join(" ")} timed out after ${timeoutMs}ms`));
    }, timeoutMs);
    const onAbort = () => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      cleanup();
      child.kill("SIGKILL");
      reject(abortError(`qmd ${args.join(" ")} aborted`));
    };
    const cleanup = () => {
      signal?.removeEventListener("abort", onAbort);
    };
    signal?.addEventListener("abort", onAbort, { once: true });
    child.stdout.on("data", (data) => {
      stdout += data.toString();
    });
    child.stderr.on("data", (data) => {
      stderr += data.toString();
    });
    child.on("error", (err) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      cleanup();
      reject(err);
    });
    child.on("close", (code) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      cleanup();
      const isVersionCheck = args.length === 1 && args[0] === "--version";
      if (code === 0 || isVersionCheck && code === 1) {
        resolve({ stdout, stderr });
      } else {
        reject(
          new Error(
            `qmd ${args.join(" ")} failed (code ${code}): ${truncateForLog(stderr || stdout)}`
          )
        );
      }
    });
  });
}
var nextJsonRpcId = 1;
var QmdDaemonSession = class {
  child = null;
  initialized = false;
  buffer = "";
  startPromise = null;
  pendingRequests = /* @__PURE__ */ new Map();
  qmdPath;
  constructor(qmdPath) {
    this.qmdPath = qmdPath;
  }
  /** Spawn the qmd mcp child process and perform MCP handshake. */
  async start() {
    if (this.child && !this.child.killed && this.initialized) {
      return true;
    }
    if (this.startPromise) {
      return this.startPromise;
    }
    this.startPromise = (async () => {
      const processAlreadyRunning = this.child != null && !this.child.killed;
      if (!processAlreadyRunning) {
        if (this.child) {
          this.cleanup({ killChild: true });
        }
        try {
          const child = launchProcess(this.qmdPath, ["mcp"], {
            env: mergeEnv({ NO_COLOR: "1" }),
            stdio: ["pipe", "pipe", "pipe"]
          });
          this.child = child;
          this.buffer = "";
          child.stdout?.on("data", (data) => {
            if (this.child !== child) return;
            this.handleStdoutData(data);
          });
          child.stderr?.on("data", (data) => {
            if (this.child !== child) return;
            const msg = data.toString().trim();
            if (msg) log.debug(`QMD mcp stderr: ${stripControlChars(msg)}`);
          });
          child.stdin?.on("error", (err) => {
            log.debug(`QMD mcp stdin error (suppressed): ${err.message}`);
          });
          child.on("error", (err) => {
            if (this.child !== child) return;
            log.debug(`QMD mcp process error: ${err.message}`);
            this.cleanup({ child });
          });
          child.on("close", (code) => {
            if (this.child !== child) return;
            log.debug(`QMD mcp process exited (code ${code})`);
            this.cleanup({ child });
          });
        } catch (err) {
          log.debug(`QMD mcp: failed to spawn process: ${err}`);
          this.cleanup({ killChild: true });
          return false;
        }
      } else {
        log.debug("QMD mcp: process already running, retrying handshake");
      }
      try {
        const result = await this.sendRequest(
          "initialize",
          {
            protocolVersion: "2024-11-05",
            capabilities: {},
            clientInfo: { name: "openclaw-remnic", version: "1.0.0" }
          },
          6e4
        );
        if (!result) {
          this.cleanup({ killChild: true });
          return false;
        }
        this.sendNotification("notifications/initialized");
        this.initialized = true;
        log.info("QMD mcp: stdio session initialized");
        return true;
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (/timed out/i.test(msg)) {
          log.debug(`QMD mcp: handshake timed out \u2014 process still loading, will retry later`);
          this.initialized = false;
        } else {
          log.debug(`QMD mcp: failed to start stdio session: ${err}`);
          this.cleanup({ killChild: true });
        }
        return false;
      } finally {
        this.startPromise = null;
      }
    })();
    return this.startPromise;
  }
  /** Call an MCP tool and return the parsed result. */
  async callTool(name, args, timeoutMs = 3e4, signal) {
    if (!this.child || this.child.killed || !this.initialized) {
      throw new Error("QMD mcp process not running");
    }
    return this.sendRequest("tools/call", { name, arguments: args }, timeoutMs, signal);
  }
  /** Kill stdio process and clear state so the next probe can restart. */
  invalidate() {
    this.cleanup({ killChild: true });
  }
  isActive() {
    return this.child !== null && !this.child.killed && this.initialized;
  }
  /** True while the process is spawned but the MCP handshake has not yet completed. */
  isLoading() {
    return this.child !== null && !this.child.killed && !this.initialized;
  }
  sendRequest(method, params, timeoutMs, signal) {
    return new Promise((resolve, reject) => {
      throwIfAborted(signal, `QMD mcp ${method} aborted before request`);
      if (!this.child || !this.child.stdin || this.child.killed) {
        reject(new Error("QMD mcp process not available"));
        return;
      }
      const id = nextJsonRpcId++;
      const timer = setTimeout(() => {
        this.pendingRequests.delete(id);
        cleanup();
        reject(new Error(`QMD mcp ${method} timed out after ${timeoutMs}ms`));
      }, timeoutMs);
      const onAbort = () => {
        clearTimeout(timer);
        this.pendingRequests.delete(id);
        cleanup();
        reject(abortError(`QMD mcp ${method} aborted`));
      };
      const cleanup = () => {
        signal?.removeEventListener("abort", onAbort);
      };
      this.pendingRequests.set(id, { resolve, reject, timer, cleanup });
      signal?.addEventListener("abort", onAbort, { once: true });
      const message = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
      this.child.stdin.write(message, (err) => {
        if (err) {
          clearTimeout(timer);
          this.pendingRequests.delete(id);
          cleanup();
          reject(new Error(`Failed to write to QMD mcp stdin: ${err.message}`));
        }
      });
    });
  }
  sendNotification(method, params) {
    if (!this.child || !this.child.stdin || this.child.killed) return;
    if (this.child.stdin.destroyed) return;
    const msg = { jsonrpc: "2.0", method };
    if (params) msg.params = params;
    try {
      this.child.stdin.write(JSON.stringify(msg) + "\n");
    } catch {
    }
  }
  handleStdoutData(data) {
    this.buffer += data.toString();
    let newlineIdx;
    while ((newlineIdx = this.buffer.indexOf("\n")) !== -1) {
      const line = this.buffer.slice(0, newlineIdx).trim();
      this.buffer = this.buffer.slice(newlineIdx + 1);
      if (!line) continue;
      try {
        const msg = JSON.parse(line);
        this.handleMessage(msg);
      } catch {
        log.debug(`QMD mcp: unparseable stdout: ${truncateForLog(line, 200)}`);
      }
    }
  }
  handleMessage(msg) {
    if (msg.id !== void 0 && msg.id !== null) {
      const pending = this.pendingRequests.get(msg.id);
      if (pending) {
        clearTimeout(pending.timer);
        this.pendingRequests.delete(msg.id);
        pending.cleanup();
        if (msg.error) {
          pending.reject(new Error(JSON.stringify(msg.error)));
        } else {
          pending.resolve(msg.result);
        }
      }
      return;
    }
    if (msg.method) {
      log.debug(`QMD mcp notification: ${msg.method}`);
    }
  }
  cleanup(opts) {
    const target = opts?.child ?? this.child;
    if (!target) return;
    if (opts?.child && this.child !== opts.child) {
      return;
    }
    if (opts?.killChild && !target.killed) {
      target.kill("SIGTERM");
    }
    this.initialized = false;
    for (const [, pending] of this.pendingRequests) {
      clearTimeout(pending.timer);
      pending.cleanup();
      pending.reject(new Error("QMD mcp process terminated"));
    }
    this.pendingRequests.clear();
    this.startPromise = null;
    this.child = null;
    this.buffer = "";
  }
};
var QMD_RESULT_LINE_RE = /^#([0-9a-fA-F]+)\s+(\d+)%\s+(.+)/;
var QMD_PATH_TITLE_RE = /^(.+?\.[a-zA-Z]{2,10})\s+-\s+(.*)$/;
function parseQmdMarkdownResultText(text, transport) {
  const results = [];
  for (const line of text.split("\n")) {
    const m = QMD_RESULT_LINE_RE.exec(line.trim());
    if (!m) continue;
    const rest = m[3];
    const pathTitleSplit = QMD_PATH_TITLE_RE.exec(rest);
    if (!pathTitleSplit) continue;
    results.push({
      docid: m[1],
      path: pathTitleSplit[1] ?? "unknown",
      snippet: "",
      score: parseInt(m[2], 10) / 100,
      transport
    });
  }
  return results;
}
function parseMcpSearchResult(result, transport = "daemon") {
  const resultObj = result;
  if (!resultObj) return [];
  const results = [];
  const pushDocs = (docs) => {
    for (const doc of docs) {
      const d = doc;
      results.push({
        docid: typeof d.docid === "string" ? d.docid.replace(/^#/, "") : "",
        path: typeof d.file === "string" ? d.file : typeof d.path === "string" ? d.path : typeof d.docid === "string" ? d.docid.replace(/^#/, "") : "unknown",
        snippet: typeof d.snippet === "string" ? d.snippet : "",
        score: typeof d.score === "number" ? d.score : 0,
        explain: parseQmdExplain(d.explain),
        transport
      });
    }
  };
  const topStructured = resultObj.structuredContent;
  const topDocs = topStructured?.results ?? topStructured?.documents;
  if (Array.isArray(topDocs)) pushDocs(topDocs);
  const content = resultObj.content;
  if (Array.isArray(content)) {
    for (const item of content) {
      const structured = item?.structuredContent;
      const docResults = structured?.results ?? structured?.documents;
      if (Array.isArray(docResults)) pushDocs(docResults);
      if (typeof item?.text === "string") {
        try {
          const parsed = JSON.parse(item.text);
          const textResults = parsed?.results ?? parsed?.documents;
          if (Array.isArray(textResults)) pushDocs(textResults);
        } catch {
          const existingKeys = new Set(results.map((r) => `${r.docid.toLowerCase()}|${r.path}`));
          const parsed = parseQmdMarkdownResultText(item.text, transport);
          for (const p of parsed) {
            const key = `${p.docid.toLowerCase()}|${p.path}`;
            if (!existingKeys.has(key)) {
              results.push(p);
              existingKeys.add(key);
            }
          }
        }
      }
    }
  }
  return results;
}
function parseQmdSearchStdout(stdout, transport = "subprocess") {
  const trimmedOut = stdout.trim();
  if (!trimmedOut || trimmedOut === "No results found.") return [];
  const parsed = JSON.parse(trimmedOut);
  if (!Array.isArray(parsed)) return [];
  return parsed.map(
    (entry) => ({
      docid: entry.docid ?? "",
      path: entry.file ?? entry.path ?? entry.docid ?? "unknown",
      snippet: entry.snippet ?? "",
      score: typeof entry.score === "number" ? entry.score : 0,
      explain: parseQmdExplain(entry.explain),
      transport
    })
  );
}
var SHARED_DAEMON_SESSIONS = /* @__PURE__ */ new Map();
function retainSharedDaemonSession(qmdPath) {
  const normalizedPath = qmdPath.trim() || "qmd";
  const existing = SHARED_DAEMON_SESSIONS.get(normalizedPath);
  if (existing) {
    existing.refs += 1;
    return existing.session;
  }
  const session = new QmdDaemonSession(normalizedPath);
  SHARED_DAEMON_SESSIONS.set(normalizedPath, {
    refs: 1,
    session
  });
  return session;
}
function releaseSharedDaemonSession(session) {
  if (!session) return;
  for (const [qmdPath, entry] of SHARED_DAEMON_SESSIONS.entries()) {
    if (entry.session !== session) continue;
    entry.refs = Math.max(0, entry.refs - 1);
    if (entry.refs === 0) {
      entry.session.invalidate();
      SHARED_DAEMON_SESSIONS.delete(qmdPath);
    }
    return;
  }
}
var QmdClient = class _QmdClient {
  constructor(collection, maxResults, opts) {
    this.collection = collection;
    this.maxResults = maxResults;
    this.slowLog = opts?.slowLog;
    this.updateTimeoutMs = opts?.updateTimeoutMs ?? 12e4;
    this.updateMinIntervalMs = Math.max(0, opts?.updateMinIntervalMs ?? 15 * 6e4);
    this.configuredQmdPath = opts?.qmdPath?.trim() ? opts.qmdPath.trim() : void 0;
    if (this.configuredQmdPath) {
      this.qmdPath = this.configuredQmdPath;
      this.qmdPathSource = "configured";
    }
    this.daemonEnabled = Boolean(opts?.daemonUrl);
    this.daemonRecheckIntervalMs = opts?.daemonRecheckIntervalMs ?? 15e3;
  }
  collection;
  maxResults;
  available = null;
  _lastUpdateFailAtMs = null;
  lastEmbedFailAtMs = null;
  lastUpdateRunAtMs = null;
  get lastUpdateFailedAtMs() {
    return this._lastUpdateFailAtMs;
  }
  get lastUpdateRanAtMs() {
    return this.lastUpdateRunAtMs;
  }
  resetUpdateThrottles() {
    this._lastUpdateFailAtMs = null;
    this.lastUpdateRunAtMs = null;
    const gs = getGlobalQmdState();
    gs.lastGlobalUpdateRunAtMs = null;
    gs.lastGlobalUpdateFailAtMs = null;
  }
  updateTimeoutMs;
  updateMinIntervalMs;
  slowLog;
  configuredQmdPath;
  qmdPathSource = "auto-path";
  cliVersion = null;
  lastCliProbeError = null;
  // Daemon mode fields
  daemonSession = null;
  daemonAvailable = false;
  daemonSessionPath = null;
  lastDaemonCheckAtMs = 0;
  daemonEnabled;
  daemonRecheckIntervalMs;
  /** Consecutive transient daemon failures before invalidating the session. */
  daemonTransientFailures = 0;
  static DAEMON_MAX_TRANSIENT_FAILURES = 3;
  qmdPath = "qmd";
  async probe() {
    const cliOk = await this.probeCli();
    if (this.daemonEnabled) {
      await this.probeDaemon();
    }
    return cliOk || this.daemonAvailable;
  }
  async probeDaemon() {
    this.lastDaemonCheckAtMs = Date.now();
    const normalizedPath = this.qmdPath.trim() || "qmd";
    if (!this.daemonSession || this.daemonSessionPath !== normalizedPath) {
      releaseSharedDaemonSession(this.daemonSession);
      this.daemonSession = retainSharedDaemonSession(normalizedPath);
      this.daemonSessionPath = normalizedPath;
    }
    try {
      const PROBE_QUICK_TIMEOUT_MS = 3e3;
      const ok = await Promise.race([
        this.daemonSession.start(),
        new Promise((resolve) => setTimeout(() => resolve(false), PROBE_QUICK_TIMEOUT_MS))
      ]);
      if (!ok) {
        const loading = this.daemonSession.isLoading();
        log.debug(`QMD daemon: stdio session not ready within ${PROBE_QUICK_TIMEOUT_MS}ms probe window${loading ? " (still loading)" : ""}`);
        this.daemonAvailable = false;
        return false;
      }
      log.info(`QMD daemon: stdio session active (collection=${this.collection})`);
      this.daemonAvailable = true;
      this.daemonTransientFailures = 0;
      return true;
    } catch (err) {
      log.debug(`QMD daemon: probe failed: ${err}`);
      this.daemonAvailable = false;
      return false;
    }
  }
  async probeCli() {
    const parseVersion = (stdout, stderr) => {
      const lines = `${stdout}
${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
      if (lines.length === 0) return null;
      const semanticLines = lines.filter((line) => parseQmdVersion(line) !== null);
      if (semanticLines.length === 0) return lines[0] ?? null;
      return semanticLines.find((line) => /\bqmd\b/i.test(line)) ?? semanticLines[0] ?? null;
    };
    const markProbeFailure = (err) => {
      this.lastCliProbeError = err instanceof Error ? err.message : String(err);
    };
    if (this.configuredQmdPath) {
      try {
        const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, this.configuredQmdPath);
        this.available = true;
        this.qmdPath = this.configuredQmdPath;
        this.qmdPathSource = "configured";
        this.cliVersion = parseVersion(result.stdout, result.stderr);
        this.lastCliProbeError = null;
        return true;
      } catch (err) {
        markProbeFailure(err);
        this.logCliProbeWarning(
          `QMD: configured qmdPath failed (${this.configuredQmdPath}): ${this.lastCliProbeError}`
        );
      }
    }
    try {
      const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, "qmd");
      this.available = true;
      this.qmdPath = "qmd";
      this.qmdPathSource = "auto-path";
      this.cliVersion = parseVersion(result.stdout, result.stderr);
      this.lastCliProbeError = null;
      return true;
    } catch (err) {
      markProbeFailure(err);
      for (const fallbackPath of QMD_FALLBACK_PATHS) {
        try {
          const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, fallbackPath);
          this.available = true;
          this.qmdPath = fallbackPath;
          this.qmdPathSource = "auto-fallback";
          this.cliVersion = parseVersion(result.stdout, result.stderr);
          this.lastCliProbeError = null;
          log.info(`QMD: found at ${fallbackPath}`);
          return true;
        } catch (fallbackErr) {
          markProbeFailure(fallbackErr);
        }
      }
      this.available = false;
      return false;
    }
  }
  logCliProbeWarning(message) {
    const state = getGlobalQmdState();
    const now = Date.now();
    const canWarn = state.lastCliWarnAtMs === null || now - state.lastCliWarnAtMs >= QMD_CLI_WARN_THROTTLE_MS;
    if (!canWarn) {
      log.debug(message);
      return;
    }
    state.lastCliWarnAtMs = now;
    if (this.daemonAvailable) {
      log.debug(message);
      return;
    }
    log.warn(message);
  }
  /** Re-probe daemon if it was down and recheck interval has elapsed. */
  async maybeProbeDaemon() {
    if (!this.daemonEnabled) return;
    if (this.daemonAvailable && this.daemonSession?.isActive()) return;
    if (this.daemonAvailable === false) {
      const elapsed = Date.now() - this.lastDaemonCheckAtMs;
      if (elapsed < this.daemonRecheckIntervalMs) return;
    }
    this.daemonAvailable = false;
    await this.probeDaemon();
  }
  isAvailable() {
    return this.available === true || this.daemonAvailable;
  }
  /** Debug string for troubleshooting availability issues. */
  debugStatus() {
    const cliPath = this.available ? this.qmdPath : this.configuredQmdPath ?? "unavailable";
    const cliVersion = this.cliVersion ?? "unknown";
    const probeError = this.lastCliProbeError ? ` cliProbeError=${this.lastCliProbeError}` : "";
    return `cli=${this.available} daemon=${this.daemonAvailable} session=${!!this.daemonSession} cliPath=${cliPath} cliPathSource=${this.qmdPathSource} cliVersion=${cliVersion}${probeError}`;
  }
  isDaemonMode() {
    return this.daemonAvailable;
  }
  dispose() {
    releaseSharedDaemonSession(this.daemonSession);
    this.daemonSession = null;
    this.daemonSessionPath = null;
    this.daemonAvailable = false;
    this.daemonTransientFailures = 0;
  }
  /**
   * Record a daemon search success — resets the transient failure counter.
   */
  recordDaemonSuccess() {
    this.daemonTransientFailures = 0;
  }
  /**
   * Handle a non-timeout, non-cancellation daemon error.
   * Tolerates up to DAEMON_MAX_TRANSIENT_FAILURES consecutive failures
   * before invalidating the session. This prevents a single transient
   * error from pushing all concurrent searches through the subprocess
   * mutex for the full recheck interval.
   */
  handleDaemonTransientError(label, err, durationMs) {
    if (!this.daemonAvailable) {
      log.debug(`QMD daemon ${label} failed after ${durationMs}ms (daemon already unavailable, ignoring): ${err}`);
      return;
    }
    this.daemonTransientFailures += 1;
    if (this.daemonTransientFailures >= _QmdClient.DAEMON_MAX_TRANSIENT_FAILURES) {
      log.debug(`QMD daemon ${label} failed after ${durationMs}ms (${this.daemonTransientFailures} consecutive failures, invalidating): ${err}`);
      this.daemonSession?.invalidate();
      this.daemonAvailable = false;
      this.daemonTransientFailures = 0;
    } else {
      log.debug(`QMD daemon ${label} failed after ${durationMs}ms (transient ${this.daemonTransientFailures}/${_QmdClient.DAEMON_MAX_TRANSIENT_FAILURES}): ${err}`);
    }
  }
  async runQmdCommand(args, timeoutMs, signal) {
    return runQmd(args, timeoutMs, this.qmdPath, signal);
  }
  supportsIntentHints() {
    return versionAtLeast(parseQmdVersion(this.cliVersion), [1, 1, 5]);
  }
  supportsExplainTraces() {
    return versionAtLeast(parseQmdVersion(this.cliVersion), [1, 1, 2]);
  }
  /**
   * QMD v2 (>= 2.0.0) uses a new MCP tool API:
   * - `search` and `vsearch` tools removed; only `query` tool exists
   * - `query` accepts `{ searches: [{ type, query }], collections?: string[] }`
   *   instead of `{ query: string, collection?: string }`
   * - `collection` (singular) → `collections` (plural array)
   */
  isQmdV2() {
    return versionAtLeast(parseQmdVersion(this.cliVersion), [2, 0, 0]);
  }
  resolveSearchOptions(options) {
    const normalized = normalizeSearchOptions(options);
    if (!normalized) return void 0;
    const resolved = {};
    if (normalized.intent && this.supportsIntentHints()) {
      resolved.intent = normalized.intent;
    }
    if (normalized.explain === true && this.supportsExplainTraces()) {
      resolved.explain = true;
    }
    return Object.keys(resolved).length > 0 ? resolved : void 0;
  }
  resolveSupportedSearchOptions(options) {
    return this.resolveSearchOptions(options);
  }
  async search(query, collection, maxResults, options, execution) {
    if (!this.isAvailable()) return [];
    const trimmed = query.trim();
    if (!trimmed) return [];
    const col = collection ?? this.collection;
    const n = maxResults ?? this.maxResults;
    const searchOptions = this.resolveSearchOptions(options);
    const optionsFingerprint = searchOptions ? JSON.stringify(searchOptions) : "";
    const cacheKey = createHash("sha256").update(`${col}:${n}:${optionsFingerprint}:${trimmed}`).digest("hex");
    const cached = getCachedQmdSearch(cacheKey);
    if (cached) {
      log.debug(`QMD search cache hit (${cached.length} results)`);
      return cached;
    }
    await this.maybeProbeDaemon();
    if (this.daemonAvailable) {
      let results;
      try {
        results = await this.searchViaDaemon(trimmed, col, n, searchOptions, execution?.signal);
      } catch (err) {
        if (isCallerCancellation(err, execution?.signal)) {
          throw isAbortError(err) ? err : abortError("QMD daemon search aborted");
        }
        throw err;
      }
      if (results !== null) {
        if (results.length === 0) {
          log.debug("QMD daemon search returned 0 results; skipping subprocess");
        }
        setCachedQmdSearch(cacheKey, results);
        return results;
      }
      log.debug("QMD daemon search timed out/failed; skipping subprocess (daemon-only mode)");
      return [];
    }
    if (this.daemonSession?.isLoading()) {
      log.debug("QMD search: daemon loading, skipping subprocess");
      return [];
    }
    const subprocessResults = await this.searchViaSubprocess(trimmed, col, n, searchOptions, execution?.signal);
    setCachedQmdSearch(cacheKey, subprocessResults);
    return subprocessResults;
  }
  async searchGlobal(query, maxResults, execution) {
    if (!this.isAvailable()) return [];
    const trimmed = query.trim();
    if (!trimmed) return [];
    const n = maxResults ?? 6;
    await this.maybeProbeDaemon();
    if (this.daemonAvailable) {
      let results;
      try {
        results = await this.searchViaDaemon(trimmed, void 0, n, void 0, execution?.signal);
      } catch (err) {
        if (isCallerCancellation(err, execution?.signal)) {
          throw isAbortError(err) ? err : abortError("QMD daemon global search aborted");
        }
        throw err;
      }
      if (results !== null) {
        if (results.length === 0) {
          log.debug("QMD daemon global search returned 0 results; skipping subprocess");
        }
        return results;
      }
      log.debug("QMD daemon global search timed out/failed; skipping subprocess (daemon-only mode)");
      return [];
    }
    if (this.daemonSession?.isLoading()) {
      log.debug("QMD searchGlobal: daemon loading, skipping subprocess");
      return [];
    }
    return this.searchGlobalViaSubprocess(trimmed, n, execution?.signal);
  }
  /**
   * BM25 keyword search (fast, ~0.3s). Uses `qmd search`.
   */
  async bm25Search(query, collection, maxResults, execution) {
    if (!this.isAvailable()) return [];
    const trimmed = query.trim();
    if (!trimmed) return [];
    const col = collection ?? this.collection;
    const n = maxResults ?? this.maxResults;
    await this.maybeProbeDaemon();
    if (this.daemonAvailable && this.daemonSession) {
      let results;
      try {
        results = await this.bm25SearchViaDaemon(trimmed, col, n, execution?.signal);
      } catch (err) {
        if (isCallerCancellation(err, execution?.signal)) {
          throw isAbortError(err) ? err : abortError("QMD daemon bm25 aborted");
        }
        throw err;
      }
      if (results !== null) {
        if (results.length === 0) {
          log.debug("QMD daemon bm25 returned 0 results; skipping subprocess");
        }
        return results;
      }
      log.debug("QMD daemon bm25 timed out/failed; skipping subprocess (daemon-only mode)");
      return [];
    }
    if (this.daemonSession?.isLoading()) {
      log.debug("QMD bm25: daemon loading, skipping subprocess");
      return [];
    }
    return this.bm25SearchViaSubprocess(trimmed, col, n, execution?.signal);
  }
  /**
   * Vector similarity search (~3-4s). Uses `qmd vsearch`.
   */
  async vectorSearch(query, collection, maxResults, execution) {
    if (!this.isAvailable()) return [];
    const trimmed = query.trim();
    if (!trimmed) return [];
    const col = collection ?? this.collection;
    const n = maxResults ?? this.maxResults;
    await this.maybeProbeDaemon();
    if (this.daemonAvailable && this.daemonSession) {
      let results;
      try {
        results = await this.vsearchViaDaemon(trimmed, col, n, execution?.signal);
      } catch (err) {
        if (isCallerCancellation(err, execution?.signal)) {
          throw isAbortError(err) ? err : abortError("QMD daemon vsearch aborted");
        }
        throw err;
      }
      if (results !== null) {
        if (results.length === 0) {
          log.debug("QMD daemon vsearch returned 0 results; skipping subprocess");
        }
        return results;
      }
      log.debug("QMD daemon vsearch timed out/failed; skipping subprocess (daemon-only mode)");
      return [];
    }
    if (this.daemonSession?.isLoading()) {
      log.debug("QMD vsearch: daemon loading, skipping subprocess");
      return [];
    }
    return this.vsearchViaSubprocess(trimmed, col, n, execution?.signal);
  }
  /**
   * Hybrid search: runs BM25 + vector in parallel, merges/dedupes by path
   * keeping the best score and first non-empty snippet.
   */
  async hybridSearch(query, collection, maxResults, execution) {
    const n = maxResults ?? this.maxResults;
    const trimmed = query.trim();
    if (!trimmed) return [];
    const [bm25Results, vectorResults] = await Promise.all([
      this.bm25Search(trimmed, collection, n, execution),
      this.vectorSearch(trimmed, collection, n, execution)
    ]);
    const merged = /* @__PURE__ */ new Map();
    for (const r of [...bm25Results, ...vectorResults]) {
      const key = r.path || r.docid;
      const existing = merged.get(key);
      if (!existing || r.score > existing.score) {
        merged.set(key, {
          ...r,
          snippet: r.snippet || existing?.snippet || ""
        });
      }
    }
    return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, n);
  }
  async searchViaDaemon(query, collection, maxResults, options, signal) {
    if (!this.daemonSession || !this.daemonAvailable) return null;
    const startedAtMs = Date.now();
    const v2 = this.isQmdV2();
    try {
      let args;
      if (v2) {
        const searches = [{ type: "lex", query }];
        searches.push({ type: "vec", query });
        args = { searches, limit: maxResults };
        if (collection) {
          args.collections = [collection];
        }
        if (options?.intent) {
          args.intent = options.intent;
        }
        if (options?.explain === true) {
          args.explain = true;
        }
      } else {
        args = { query, limit: maxResults };
        if (collection) {
          args.collection = collection;
        }
        if (options?.intent) {
          args.intent = options.intent;
        }
        if (options?.explain === true) {
          args.explain = true;
        }
      }
      const result = await this.daemonSession.callTool("query", args, QMD_DAEMON_TIMEOUT_MS, signal);
      const durationMs = Date.now() - startedAtMs;
      if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
        log.warn(
          `SLOW QMD daemon query: durationMs=${durationMs} collection=${collection ?? "global"} maxResults=${maxResults} queryChars=${query.length} v2=${v2}`
        );
      }
      const results = parseMcpSearchResult(result, "daemon");
      log.debug(`QMD daemon search: ${results.length} results in ${durationMs}ms (v2=${v2})`);
      this.recordDaemonSuccess();
      return results;
    } catch (err) {
      const durationMs = Date.now() - startedAtMs;
      if (isCallerCancellation(err, signal)) {
        log.debug(`QMD daemon search aborted/cancelled after ${durationMs}ms`);
        throw isAbortError(err) ? err : abortError("QMD daemon search aborted");
      }
      if (isDaemonTimeoutError(err)) {
        log.debug(`QMD daemon search timed out after ${durationMs}ms, falling back to subprocess`);
        return null;
      }
      this.handleDaemonTransientError("search", err, durationMs);
      return null;
    }
  }
  async bm25SearchViaDaemon(query, collection, maxResults, signal) {
    if (!this.daemonSession || !this.daemonAvailable) return null;
    const startedAtMs = Date.now();
    const v2 = this.isQmdV2();
    try {
      let result;
      if (v2) {
        result = await this.daemonSession.callTool(
          "query",
          {
            searches: [{ type: "lex", query }],
            collections: [collection],
            limit: maxResults
          },
          QMD_DAEMON_TIMEOUT_MS,
          signal
        );
      } else {
        result = await this.daemonSession.callTool(
          "search",
          { query, limit: maxResults, collection },
          QMD_DAEMON_TIMEOUT_MS,
          signal
        );
      }
      const durationMs = Date.now() - startedAtMs;
      const results = parseMcpSearchResult(result);
      log.debug(`QMD daemon bm25: ${results.length} results in ${durationMs}ms (v2=${v2})`);
      this.recordDaemonSuccess();
      return results;
    } catch (err) {
      const durationMs = Date.now() - startedAtMs;
      if (isCallerCancellation(err, signal)) {
        log.debug(`QMD daemon bm25 aborted/cancelled after ${durationMs}ms`);
        throw isAbortError(err) ? err : abortError("QMD daemon bm25 aborted");
      }
      if (isDaemonTimeoutError(err)) {
        log.debug(`QMD daemon bm25 timed out after ${durationMs}ms, falling back to subprocess`);
        return null;
      }
      this.handleDaemonTransientError("bm25", err, durationMs);
      return null;
    }
  }
  async vsearchViaDaemon(query, collection, maxResults, signal) {
    if (!this.daemonSession || !this.daemonAvailable) return null;
    const startedAtMs = Date.now();
    const v2 = this.isQmdV2();
    try {
      let result;
      if (v2) {
        result = await this.daemonSession.callTool(
          "query",
          {
            searches: [{ type: "vec", query }],
            collections: [collection],
            limit: maxResults
          },
          QMD_DAEMON_TIMEOUT_MS,
          signal
        );
      } else {
        result = await this.daemonSession.callTool(
          "vsearch",
          { query, limit: maxResults, collection },
          QMD_DAEMON_TIMEOUT_MS,
          signal
        );
      }
      const durationMs = Date.now() - startedAtMs;
      const results = parseMcpSearchResult(result);
      log.debug(`QMD daemon vsearch: ${results.length} results in ${durationMs}ms (v2=${v2})`);
      this.recordDaemonSuccess();
      return results;
    } catch (err) {
      const durationMs = Date.now() - startedAtMs;
      if (isCallerCancellation(err, signal)) {
        log.debug(`QMD daemon vsearch aborted/cancelled after ${durationMs}ms`);
        throw isAbortError(err) ? err : abortError("QMD daemon vsearch aborted");
      }
      if (isDaemonTimeoutError(err)) {
        log.debug(`QMD daemon vsearch timed out after ${durationMs}ms, falling back to subprocess`);
        return null;
      }
      this.handleDaemonTransientError("vsearch", err, durationMs);
      return null;
    }
  }
  async searchViaSubprocess(query, collection, maxResults, options, signal) {
    if (this.available === false) return [];
    const startedAtMs = Date.now();
    try {
      const args = ["query", query, "-c", collection, "--json", "-n", String(maxResults)];
      if (options?.intent) {
        args.push("--intent", options.intent);
      }
      if (options?.explain === true) {
        args.push("--explain");
      }
      const { stdout } = await runQmd(
        args,
        QMD_TIMEOUT_MS,
        this.qmdPath,
        signal
      );
      const durationMs = Date.now() - startedAtMs;
      if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
        log.warn(
          `SLOW QMD query: durationMs=${durationMs} collection=${collection} maxResults=${maxResults} queryChars=${query.length}`
        );
      }
      return parseQmdSearchStdout(stdout, "subprocess");
    } catch (err) {
      if (isCallerCancellation(err, signal)) {
        throw isAbortError(err) ? err : abortError("QMD subprocess search aborted");
      }
      log.debug(`QMD search failed: ${err}`);
      return [];
    }
  }
  async bm25SearchViaSubprocess(query, collection, maxResults, signal) {
    if (this.available === false) return [];
    const startedAtMs = Date.now();
    try {
      const { stdout } = await runQmd(
        ["search", query, "-c", collection, "--json", "-n", String(maxResults)],
        QMD_TIMEOUT_MS,
        this.qmdPath,
        signal
      );
      log.debug(`QMD bm25: ${Date.now() - startedAtMs}ms`);
      return parseQmdSearchStdout(stdout);
    } catch (err) {
      if (isCallerCancellation(err, signal)) {
        throw isAbortError(err) ? err : abortError("QMD subprocess bm25 aborted");
      }
      log.debug(`QMD bm25 search failed: ${err}`);
      return [];
    }
  }
  async vsearchViaSubprocess(query, collection, maxResults, signal) {
    if (this.available === false) return [];
    const startedAtMs = Date.now();
    try {
      const { stdout } = await runQmd(
        ["vsearch", query, "-c", collection, "--json", "-n", String(maxResults)],
        QMD_TIMEOUT_MS,
        this.qmdPath,
        signal
      );
      log.debug(`QMD vsearch: ${Date.now() - startedAtMs}ms`);
      return parseQmdSearchStdout(stdout);
    } catch (err) {
      if (isCallerCancellation(err, signal)) {
        throw isAbortError(err) ? err : abortError("QMD subprocess vsearch aborted");
      }
      log.debug(`QMD vsearch failed: ${err}`);
      return [];
    }
  }
  async searchGlobalViaSubprocess(query, maxResults, signal) {
    if (this.available === false) return [];
    const startedAtMs = Date.now();
    try {
      const { stdout } = await runQmd(
        ["query", query, "--json", "-n", String(maxResults)],
        QMD_TIMEOUT_MS,
        this.qmdPath,
        signal
      );
      const durationMs = Date.now() - startedAtMs;
      if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
        log.warn(
          `SLOW QMD global query: durationMs=${durationMs} maxResults=${maxResults} queryChars=${query.length}`
        );
      }
      return parseQmdSearchStdout(stdout);
    } catch (err) {
      if (isCallerCancellation(err, signal)) {
        throw isAbortError(err) ? err : abortError("QMD subprocess global search aborted");
      }
      log.debug(`QMD global search failed: ${err}`);
      return [];
    }
  }
  async update(execution) {
    await this.runUpdateForCollection(
      this.collection,
      { perCollectionThrottle: false },
      execution?.signal
    );
  }
  async updateCollection(collection, execution) {
    await this.runUpdateForCollection(
      collection,
      { perCollectionThrottle: true },
      execution?.signal
    );
  }
  async updateCollectionStrict(collection, execution) {
    await this.runUpdateForCollection(
      collection,
      { perCollectionThrottle: true, strict: true },
      execution?.signal
    );
  }
  updatesAllCollections() {
    return true;
  }
  async runUpdateForCollection(collection, options, signal) {
    if (this.available === false) {
      if (options.strict) {
        throw new Error("QMD unavailable");
      }
      return;
    }
    const name = collection.trim();
    if (!name) {
      if (options.strict) {
        throw new Error("QMD collection name is required");
      }
      return;
    }
    const globalState = getGlobalQmdState();
    const now = Date.now();
    if (!options.strict && options.perCollectionThrottle) {
      if (globalState.lastGlobalUpdateFailAtMs && now - globalState.lastGlobalUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
        log.debug("QMD update: suppressed by global failure backoff");
        return;
      }
      const lastCollectionRun = globalState.lastUpdateByCollectionMs[name];
      if (Number.isFinite(lastCollectionRun) && now - lastCollectionRun < this.updateMinIntervalMs) {
        log.debug(`QMD update: suppressed by per-collection min-interval gate (${name})`);
        return;
      }
      const lastCollectionFail = globalState.lastUpdateFailByCollectionMs[name];
      if (Number.isFinite(lastCollectionFail) && now - lastCollectionFail < QMD_UPDATE_BACKOFF_MS) {
        log.debug(`QMD update: suppressed by per-collection failure backoff (${name})`);
        return;
      }
    } else if (!options.strict) {
      if (this.lastUpdateRunAtMs && now - this.lastUpdateRunAtMs < this.updateMinIntervalMs) {
        log.debug("QMD update: suppressed due to min-interval gate");
        return;
      }
      if (this._lastUpdateFailAtMs && now - this._lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
        log.debug("QMD update: suppressed due to recent failures (backoff)");
        return;
      }
      if (globalState.lastGlobalUpdateRunAtMs && now - globalState.lastGlobalUpdateRunAtMs < this.updateMinIntervalMs) {
        log.debug("QMD update: suppressed by global min-interval gate");
        return;
      }
      if (globalState.lastGlobalUpdateFailAtMs && now - globalState.lastGlobalUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
        log.debug("QMD update: suppressed by global failure backoff");
        return;
      }
    }
    try {
      if (!globalState.warnedGlobalUpdateBehavior) {
        globalState.warnedGlobalUpdateBehavior = true;
        log.warn(
          "QMD update runs globally across collections in current CLI versions; Engram now rate-limits update calls to reduce gateway load."
        );
      }
      const startedAtMs = Date.now();
      await this.runQmdCommand(["update", "-c", name], this.updateTimeoutMs, signal);
      const durationMs = Date.now() - startedAtMs;
      if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
        log.warn(`SLOW QMD update: durationMs=${durationMs}`);
      }
      const at = Date.now();
      if (options.perCollectionThrottle) {
        globalState.lastUpdateByCollectionMs[name] = at;
        globalState.lastGlobalUpdateRunAtMs = at;
      } else {
        this.lastUpdateRunAtMs = at;
        globalState.lastGlobalUpdateRunAtMs = at;
      }
      log.debug(`QMD update completed for collection=${name}`);
    } catch (err) {
      const at = Date.now();
      if (options.perCollectionThrottle) {
        globalState.lastUpdateFailByCollectionMs[name] = at;
        globalState.lastGlobalUpdateFailAtMs = at;
      } else {
        this._lastUpdateFailAtMs = at;
        globalState.lastGlobalUpdateFailAtMs = at;
      }
      const msg = err instanceof Error ? err.message : String(err);
      log.warn(`QMD update failed for collection ${name}: ${msg}`);
      if (options.strict) {
        throw err;
      }
    }
  }
  async embed() {
    if (this.available === false) return;
    const globalState = getGlobalQmdState();
    if (this.lastEmbedFailAtMs && Date.now() - this.lastEmbedFailAtMs < QMD_EMBED_BACKOFF_MS) {
      log.debug("QMD embed: suppressed due to recent failures (backoff)");
      return;
    }
    if (globalState.lastGlobalEmbedRunAtMs && Date.now() - globalState.lastGlobalEmbedRunAtMs < this.updateMinIntervalMs) {
      log.debug("QMD embed: suppressed by global min-interval gate");
      return;
    }
    if (globalState.lastGlobalEmbedFailAtMs && Date.now() - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS) {
      log.debug("QMD embed: suppressed by global failure backoff");
      return;
    }
    try {
      const startedAtMs = Date.now();
      await this.runQmdCommand(["embed", "-c", this.collection], 3e5);
      const durationMs = Date.now() - startedAtMs;
      if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
        log.warn(`SLOW QMD embed: durationMs=${durationMs}`);
      }
      globalState.lastGlobalEmbedRunAtMs = Date.now();
      log.debug("QMD embed completed");
    } catch (err) {
      if (isVectorDimensionMismatchError(err)) {
        try {
          log.warn("QMD embed hit a vector dimension mismatch; retrying with force re-embed");
          await this.runQmdCommand(["embed", "-f", "-c", this.collection], 3e5);
          globalState.lastGlobalEmbedRunAtMs = Date.now();
          this.lastEmbedFailAtMs = null;
          globalState.lastGlobalEmbedFailAtMs = null;
          log.warn("QMD embed recovered by forcing a full vector rebuild");
          return;
        } catch (retryErr) {
          const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
          log.warn(`QMD force re-embed failed after dimension mismatch: ${retryMsg}`);
        }
      }
      const now = Date.now();
      this.lastEmbedFailAtMs = now;
      globalState.lastGlobalEmbedFailAtMs = now;
      const msg = err instanceof Error ? err.message : String(err);
      log.warn(`QMD embed failed: ${msg}`);
    }
  }
  async embedCollection(collection) {
    if (this.available === false) return;
    const name = collection.trim();
    if (!name) return;
    const globalState = getGlobalQmdState();
    const now = Date.now();
    if (globalState.lastGlobalEmbedFailAtMs && now - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS) {
      log.debug(`QMD embed: suppressed by global failure backoff (${name})`);
      return;
    }
    const lastCollectionRun = globalState.lastEmbedByCollectionMs[name];
    if (Number.isFinite(lastCollectionRun) && now - lastCollectionRun < this.updateMinIntervalMs) {
      log.debug(`QMD embed: suppressed by per-collection min-interval gate (${name})`);
      return;
    }
    const lastCollectionFail = globalState.lastEmbedFailByCollectionMs[name];
    if (Number.isFinite(lastCollectionFail) && now - lastCollectionFail < QMD_EMBED_BACKOFF_MS) {
      log.debug(`QMD embed: suppressed by per-collection failure backoff (${name})`);
      return;
    }
    try {
      await this.runQmdCommand(["embed", "-c", name], 3e5);
      const at = Date.now();
      globalState.lastEmbedByCollectionMs[name] = at;
      globalState.lastGlobalEmbedRunAtMs = at;
    } catch (err) {
      if (isVectorDimensionMismatchError(err)) {
        try {
          log.warn(`QMD embed for collection ${name} hit a vector dimension mismatch; retrying with force re-embed`);
          await this.runQmdCommand(["embed", "-f", "-c", name], 3e5);
          const recoveredAt = Date.now();
          globalState.lastEmbedByCollectionMs[name] = recoveredAt;
          globalState.lastGlobalEmbedRunAtMs = recoveredAt;
          delete globalState.lastEmbedFailByCollectionMs[name];
          globalState.lastGlobalEmbedFailAtMs = null;
          log.warn(`QMD embed for collection ${name} recovered by forcing a full vector rebuild`);
          return;
        } catch (retryErr) {
          const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
          log.warn(`QMD force re-embed failed for collection ${name}: ${retryMsg}`);
        }
      }
      const at = Date.now();
      globalState.lastEmbedFailByCollectionMs[name] = at;
      globalState.lastGlobalEmbedFailAtMs = at;
      const msg = err instanceof Error ? err.message : String(err);
      log.warn(`QMD embed failed for collection ${name}: ${msg}`);
    }
  }
  async ensureCollection(memoryDir) {
    if (this.available === false && !this.daemonAvailable) return "unknown";
    if (this.available === false) return "skipped";
    try {
      const { stdout } = await runQmd(
        ["collection", "list"],
        QMD_TIMEOUT_MS,
        this.qmdPath
      );
      const collectionRegex = new RegExp(
        `^${this.collection}\\s+\\(qmd://`,
        "m"
      );
      if (collectionRegex.test(stdout)) {
        return "present";
      }
    } catch (err) {
      log.debug(
        `QMD collection check unavailable for "${this.collection}" (will not disable features): ${err instanceof Error ? err.message : String(err)}`
      );
      return "unknown";
    }
    log.info(
      `QMD collection "${this.collection}" not found. Add it to ~/.config/qmd/index.yml pointing at ${memoryDir}`
    );
    return "missing";
  }
};

export {
  parseQmdExplain,
  QmdClient
};
//# sourceMappingURL=chunk-P77UEOU2.js.map