import { g as resolveStateDir } from "./paths-mmi2xb99.js";
import "./globals-D5pM_phR.js";
import "./utils-3Lue6wVm.js";
import { t as createSubsystemLogger } from "./subsystem-_lp1q2Mi.js";
import { H as parseAgentSessionKey, s as resolveAgentWorkspaceDir } from "./agent-scope-u760_XKU.js";
import "./exec-NeyMP-Tl.js";
import "./edwinpai-root-fGTEvnPp.js";
import { n as isMemoryPathAllowed, t as isDirectMemoryReadAllowed } from "./access-policy-Bz60GPHK.js";
import { s as hashText } from "./internal-C9ERe0oQ.js";
import { a as resolveSessionTranscriptsDirForAgent } from "./paths-CzSw2xi4.js";
import { n as redactSensitiveText } from "./redact-DKKIzZKH.js";
import { t as requireNodeSqlite } from "./sqlite-BE2ba2U3.js";
import path from "node:path";
import os from "node:os";
import fs from "node:fs/promises";
import { spawn } from "node:child_process";

//#region ../../src/memory/qmd-pool.ts
/**
* QMD Process Pool — bounded concurrency for qmd subprocess invocations.
*
* Instead of spawning unlimited concurrent `qmd` processes (each consuming
* 6-8% CPU with cold-start overhead), this pool enforces a hard cap on
* concurrent workers and queues excess requests.
*
* Think of it like a restaurant: keep N kitchens running, queue customers
* when all kitchens are busy, never build kitchen N+1.
*/
const log$2 = createSubsystemLogger("qmd-pool");
var QmdProcessPool = class {
	constructor(opts) {
		this.active = 0;
		this.queue = [];
		this.totalCompleted = 0;
		this.totalFailed = 0;
		this.totalTimedOut = 0;
		this.totalDurationMs = 0;
		this.closed = false;
		this.activeProcesses = /* @__PURE__ */ new Set();
		this.command = opts.command;
		this.maxWorkers = Math.max(1, opts.maxWorkers ?? 3);
		this.maxQueueSize = Math.max(0, opts.maxQueueSize ?? 20);
	}
	/**
	* Execute a qmd command, respecting pool concurrency limits.
	* If all workers are busy, the request is queued.
	* If the queue is full, the request is rejected immediately.
	*/
	async exec(args, opts) {
		if (this.closed) throw new Error("QMD pool is closed");
		if (this.active < this.maxWorkers) return this.runProcess(args, opts);
		if (this.queue.length >= this.maxQueueSize) throw new Error(`QMD pool queue full (${this.queue.length}/${this.maxQueueSize}). ${this.active} workers busy. Try again later.`);
		return new Promise((resolve, reject) => {
			this.queue.push({
				args,
				env: opts.env,
				cwd: opts.cwd,
				timeoutMs: opts.timeoutMs,
				resolve,
				reject,
				queuedAt: Date.now()
			});
			log$2.info(`qmd queued (active=${this.active}/${this.maxWorkers}, queue=${this.queue.length}): ${args.join(" ")}`);
		});
	}
	/** Get current pool statistics. */
	stats() {
		return {
			active: this.active,
			queued: this.queue.length,
			maxWorkers: this.maxWorkers,
			totalCompleted: this.totalCompleted,
			totalFailed: this.totalFailed,
			totalTimedOut: this.totalTimedOut,
			avgDurationMs: this.totalCompleted > 0 ? Math.round(this.totalDurationMs / this.totalCompleted) : 0
		};
	}
	/** Shut down the pool, killing any active processes and draining the queue. */
	async close() {
		if (this.closed) return;
		this.closed = true;
		for (const job of this.queue) job.reject(/* @__PURE__ */ new Error("QMD pool closed"));
		this.queue.length = 0;
		for (const proc of this.activeProcesses) proc.kill("SIGKILL");
		this.activeProcesses.clear();
	}
	async runProcess(args, opts) {
		this.active += 1;
		const startTime = Date.now();
		try {
			const result = await this.spawnProcess(args, opts);
			this.totalCompleted += 1;
			this.totalDurationMs += Date.now() - startTime;
			return result;
		} catch (err) {
			if (err instanceof Error && err.message.includes("timed out")) this.totalTimedOut += 1;
			else this.totalFailed += 1;
			throw err;
		} finally {
			this.active -= 1;
			this.drainQueue();
		}
	}
	spawnProcess(args, opts) {
		return this.spawnProcessAttempt(this.command, args, opts, false);
	}
	spawnProcessAttempt(command, args, opts, viaBash) {
		return new Promise((resolve, reject) => {
			const child = spawn(viaBash ? "/bin/bash" : command, viaBash ? [command, ...args] : args, {
				env: opts.env,
				cwd: opts.cwd
			});
			this.activeProcesses.add(child);
			let stdout = "";
			let stderr = "";
			let settled = false;
			const timer = opts.timeoutMs ? setTimeout(() => {
				if (!settled) {
					settled = true;
					child.kill("SIGKILL");
					this.activeProcesses.delete(child);
					reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
				}
			}, opts.timeoutMs) : null;
			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;
				if (timer) clearTimeout(timer);
				this.activeProcesses.delete(child);
				if (!viaBash && err.code === "ENOENT" && command.includes("/")) {
					log$2.warn(`qmd exec ENOENT for ${command}; cwd=${opts.cwd}; path=${opts.env.PATH ?? "<missing>"}; retrying via /bin/bash`);
					this.spawnProcessAttempt(command, args, opts, true).then(resolve, reject);
					return;
				}
				reject(err);
			});
			child.on("close", (code) => {
				if (!settled) {
					settled = true;
					if (timer) clearTimeout(timer);
					this.activeProcesses.delete(child);
					if (code === 0) resolve({
						stdout,
						stderr
					});
					else reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} failed (code ${code}): ${stderr || stdout}`));
				}
			});
		});
	}
	drainQueue() {
		while (this.queue.length > 0 && this.active < this.maxWorkers && !this.closed) {
			const job = this.queue.shift();
			if (!job) break;
			const waitTime = Date.now() - job.queuedAt;
			log$2.info(`qmd dequeued after ${waitTime}ms wait (active=${this.active + 1}/${this.maxWorkers})`);
			this.runProcess(job.args, {
				env: job.env,
				cwd: job.cwd,
				timeoutMs: job.timeoutMs
			}).then(job.resolve, job.reject);
		}
	}
};

//#endregion
//#region ../../src/memory/session-files.ts
const log$1 = createSubsystemLogger("memory");
async function listSessionFilesForAgent(agentId) {
	const dir = resolveSessionTranscriptsDirForAgent(agentId);
	try {
		return (await fs.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => name.endsWith(".jsonl")).map((name) => path.join(dir, name));
	} catch {
		return [];
	}
}
function sessionPathForFile(absPath) {
	return path.join("sessions", path.basename(absPath)).replace(/\\/g, "/");
}
function normalizeSessionText(value) {
	return value.replace(/\s*\n+\s*/g, " ").replace(/\s+/g, " ").trim();
}
function extractSessionText(content) {
	if (typeof content === "string") {
		const normalized = normalizeSessionText(content);
		return normalized ? normalized : null;
	}
	if (!Array.isArray(content)) return null;
	const parts = [];
	for (const block of content) {
		if (!block || typeof block !== "object") continue;
		const record = block;
		if (record.type !== "text" || typeof record.text !== "string") continue;
		const normalized = normalizeSessionText(record.text);
		if (normalized) parts.push(normalized);
	}
	if (parts.length === 0) return null;
	return parts.join(" ");
}
async function buildSessionEntry(absPath) {
	try {
		const stat = await fs.stat(absPath);
		const lines = (await fs.readFile(absPath, "utf-8")).split("\n");
		const collected = [];
		for (const line of lines) {
			if (!line.trim()) continue;
			let record;
			try {
				record = JSON.parse(line);
			} catch {
				continue;
			}
			if (!record || typeof record !== "object" || record.type !== "message") continue;
			const message = record.message;
			if (!message || typeof message.role !== "string") continue;
			if (message.role !== "user" && message.role !== "assistant") continue;
			const text = extractSessionText(message.content);
			if (!text) continue;
			const safe = redactSensitiveText(text, { mode: "tools" });
			const label = message.role === "user" ? "User" : "Assistant";
			collected.push(`${label}: ${safe}`);
		}
		const content = collected.join("\n");
		return {
			path: sessionPathForFile(absPath),
			absPath,
			mtimeMs: stat.mtimeMs,
			size: stat.size,
			hash: hashText(content),
			content
		};
	} catch (err) {
		log$1.debug(`Failed reading session file ${absPath}: ${String(err)}`);
		return null;
	}
}

//#endregion
//#region ../../src/memory/temporal.ts
const DEFAULT_TEMPORAL_CONFIG = {
	halfLifeDays: 30,
	maxAgeDays: 365,
	temporalAlpha: .2,
	decayCurve: "exponential"
};
/**
* Compute a temporal decay score between 0 and 1.
* 1.0 = just happened, 0.0 = older than maxAgeDays.
*/
function computeDecayScore(eventTime, now, config = DEFAULT_TEMPORAL_CONFIG) {
	const ageMs = now.getTime() - eventTime.getTime();
	if (ageMs <= 0) return 1;
	const ageDays = ageMs / (1e3 * 60 * 60 * 24);
	if (ageDays >= config.maxAgeDays) return 0;
	if (config.decayCurve === "linear") return Math.max(0, 1 - ageDays / config.maxAgeDays);
	const decay = Math.pow(2, -ageDays / config.halfLifeDays);
	return Math.max(0, Math.min(1, decay));
}
/**
* Blend a relevance score with a temporal decay score.
* Returns a score in [0, 1].
*/
function blendTemporalScore(relevanceScore, temporalScore, alpha = DEFAULT_TEMPORAL_CONFIG.temporalAlpha) {
	const clamped = Math.max(0, Math.min(1, alpha));
	return (1 - clamped) * relevanceScore + clamped * temporalScore;
}
/**
* Compute an access-frequency boost for a memory record.
* More frequently accessed memories get a small boost.
* Returns a value in [0, 0.1] to add to the base score.
*/
function accessFrequencyBoost(accessCount, maxBoost = .1) {
	if (accessCount <= 0) return 0;
	return Math.min(maxBoost, maxBoost * (Math.log2(accessCount + 1) / 10));
}

//#endregion
//#region ../../src/memory/qmd-manager.ts
const log = createSubsystemLogger("memory");
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
const CONSOLIDATE_UPDATE_THRESHOLD = 10;
const CONSOLIDATE_FRAGMENT_THRESHOLD = 200;
var QmdMemoryManager = class QmdMemoryManager {
	static async create(params) {
		const resolved = params.resolved.qmd;
		if (!resolved) return null;
		const manager = new QmdMemoryManager({
			cfg: params.cfg,
			agentId: params.agentId,
			resolved
		});
		await manager.initialize();
		return manager;
	}
	constructor(params) {
		this.collectionRoots = /* @__PURE__ */ new Map();
		this.sources = /* @__PURE__ */ new Set();
		this.docPathCache = /* @__PURE__ */ new Map();
		this.updateTimer = null;
		this.embedTimer = null;
		this.consolidateTimer = null;
		this.pendingUpdate = null;
		this.pendingEmbed = null;
		this.pendingConsolidate = null;
		this.closed = false;
		this.db = null;
		this.lastUpdateAt = null;
		this.lastEmbedAt = null;
		this.lastConsolidateAt = null;
		this.updateCountSinceLastConsolidate = 0;
		this.indexChangedCallbacks = [];
		this.newTokensSinceLastEmbed = 0;
		this.cfg = params.cfg;
		this.agentId = params.agentId;
		this.qmd = params.resolved;
		this.workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
		this.stateDir = resolveStateDir(process.env, os.homedir);
		this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
		this.qmdDir = path.join(this.agentStateDir, "qmd");
		this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
		this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache");
		this.indexPath = path.join(this.xdgCacheHome, "qmd", "index.sqlite");
		const openAiApiKey = params.resolved.embeddingApiKey?.trim();
		this.env = {
			...process.env,
			...openAiApiKey ? {
				OPENAI_API_KEY: openAiApiKey,
				QMD_OPENAI: "1"
			} : {},
			XDG_CONFIG_HOME: this.xdgConfigHome,
			XDG_CACHE_HOME: this.xdgCacheHome,
			NO_COLOR: "1"
		};
		this.pool = new QmdProcessPool({
			command: this.qmd.command,
			maxWorkers: 3,
			maxQueueSize: 20
		});
		this.sessionExporter = this.qmd.sessions.enabled ? {
			dir: this.qmd.sessions.exportDir ?? path.join(this.qmdDir, "sessions"),
			retentionMs: this.qmd.sessions.retentionDays ? this.qmd.sessions.retentionDays * 24 * 60 * 60 * 1e3 : void 0,
			collectionName: this.pickSessionCollectionName()
		} : null;
		if (this.sessionExporter) this.qmd.collections = [...this.qmd.collections, {
			name: this.sessionExporter.collectionName,
			path: this.sessionExporter.dir,
			pattern: "**/*.md",
			kind: "sessions"
		}];
	}
	async initialize() {
		await fs.mkdir(this.xdgConfigHome, { recursive: true });
		await fs.mkdir(this.xdgCacheHome, { recursive: true });
		await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
		this.bootstrapCollections();
		await this.ensureCollections();
		if (this.qmd.update.onBoot) await this.runUpdate("boot", true);
		if (this.qmd.update.intervalMs > 0) this.updateTimer = setInterval(() => {
			this.runUpdate("interval").catch((err) => {
				log.warn(`qmd update failed (${String(err)})`);
			});
		}, this.qmd.update.intervalMs);
		if (this.qmd.update.embedMaxIntervalMs > 0) this.embedTimer = setInterval(() => {
			this.maybeScheduleEmbed("embed-ceiling");
		}, this.qmd.update.embedMaxIntervalMs);
		if (this.qmd.consolidate.onBoot) await this.runConsolidation(true);
		if (this.qmd.consolidate.intervalMs > 0) this.consolidateTimer = setInterval(() => {
			this.runConsolidation().catch((err) => {
				log.warn(`qmd consolidate failed (${String(err)})`);
			});
		}, this.qmd.consolidate.intervalMs);
	}
	bootstrapCollections() {
		this.collectionRoots.clear();
		this.sources.clear();
		for (const collection of this.qmd.collections) {
			const kind = collection.kind === "sessions" ? "sessions" : "memory";
			this.collectionRoots.set(collection.name, {
				path: collection.path,
				kind
			});
			this.sources.add(kind);
		}
	}
	async ensureCollections() {
		const existing = /* @__PURE__ */ new Set();
		try {
			const result = await this.runQmd([
				"collection",
				"list",
				"--json"
			]);
			const parsed = JSON.parse(result.stdout);
			if (Array.isArray(parsed)) {
				for (const entry of parsed) if (typeof entry === "string") existing.add(entry);
				else if (entry && typeof entry === "object") {
					const name = entry.name;
					if (typeof name === "string") existing.add(name);
				}
			}
		} catch {}
		for (const collection of this.qmd.collections) {
			if (existing.has(collection.name)) continue;
			try {
				await this.runQmd([
					"collection",
					"add",
					collection.path,
					"--name",
					collection.name,
					"--mask",
					collection.pattern
				]);
			} catch (err) {
				const message = err instanceof Error ? err.message : String(err);
				if (message.toLowerCase().includes("already exists")) continue;
				if (message.toLowerCase().includes("exists")) continue;
				log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
			}
		}
	}
	async search(query, opts) {
		if (!this.isScopeAllowed(opts?.sessionKey)) return [];
		const trimmed = query.trim();
		if (!trimmed) return [];
		await this.pendingUpdate?.catch(() => void 0);
		const limit = Math.min(this.qmd.limits.maxResults, opts?.maxResults ?? this.qmd.limits.maxResults);
		const args = [
			"query",
			trimmed,
			"--json",
			"-n",
			String(limit)
		];
		let stdout;
		try {
			stdout = (await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs })).stdout;
		} catch (err) {
			log.warn(`qmd query failed: ${String(err)}`);
			throw err instanceof Error ? err : new Error(String(err));
		}
		let parsed = [];
		try {
			parsed = JSON.parse(stdout);
		} catch (err) {
			const message = err instanceof Error ? err.message : String(err);
			log.warn(`qmd query returned invalid JSON: ${message}`);
			throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
		}
		const results = [];
		for (const entry of parsed) {
			const doc = await this.resolveDocLocation(entry.docid);
			if (!doc) continue;
			if (!this.isDocumentAllowed(doc.rel, opts?.accessScope)) continue;
			const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? "";
			const lines = this.extractSnippetLines(snippet);
			const score = typeof entry.score === "number" ? entry.score : 0;
			if (score < (opts?.minScore ?? 0)) continue;
			const eventTime = (typeof doc.eventTimeMs === "number" ? new Date(doc.eventTimeMs) : void 0) ?? this.extractDateFromPath(doc.rel);
			const temporalScore = eventTime ? computeDecayScore(eventTime, /* @__PURE__ */ new Date()) : void 0;
			const freqBoost = accessFrequencyBoost(doc.accessCount ?? 0);
			const baseWithBoost = Math.min(1, score + freqBoost);
			const blended = temporalScore !== void 0 ? blendTemporalScore(baseWithBoost, temporalScore, DEFAULT_TEMPORAL_CONFIG.temporalAlpha) : baseWithBoost;
			results.push({
				path: doc.rel,
				startLine: lines.startLine,
				endLine: lines.endLine,
				score: blended,
				snippet,
				source: doc.source,
				eventTime: eventTime ?? void 0,
				temporalScore,
				tier: doc.memoryTier
			});
		}
		results.sort((a, b) => b.score - a.score);
		const clamped = this.clampResultsByInjectedChars(results.slice(0, limit));
		this.updateAccessCounts(clamped.map((r) => r.path));
		return clamped;
	}
	async sync(params) {
		if (params?.progress) params.progress({
			completed: 0,
			total: 1,
			label: "Updating QMD index…"
		});
		await this.runUpdate(params?.reason ?? "manual", params?.force);
		if (params?.progress) params.progress({
			completed: 1,
			total: 1,
			label: "QMD index updated"
		});
	}
	async readFile(params) {
		const relPath = params.relPath?.trim();
		if (!relPath) throw new Error("path required");
		if (!isDirectMemoryReadAllowed(params.accessScope)) throw new Error("memory_get is disabled by runtime attachment policy attach-on-demand");
		if (!this.isDocumentAllowed(relPath, params.accessScope)) throw new Error("memory path is outside allowed collections");
		const absPath = this.resolveReadPath(relPath);
		if (!absPath.endsWith(".md")) throw new Error("path required");
		const stat = await fs.lstat(absPath);
		if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("path required");
		const content = await fs.readFile(absPath, "utf-8");
		if (!params.from && !params.lines) return {
			text: content,
			path: relPath
		};
		const lines = content.split("\n");
		const start = Math.max(1, params.from ?? 1);
		const count = Math.max(1, params.lines ?? lines.length);
		return {
			text: lines.slice(start - 1, start - 1 + count).join("\n"),
			path: relPath
		};
	}
	status() {
		const counts = this.readCounts();
		const embeddingCoverage = this.readEmbeddingCoverage();
		return {
			backend: "qmd",
			provider: "qmd",
			model: "qmd",
			requestedProvider: "qmd",
			files: counts.totalDocuments,
			chunks: counts.totalDocuments,
			dirty: false,
			workspaceDir: this.workspaceDir,
			dbPath: this.indexPath,
			sources: Array.from(this.sources),
			sourceCounts: counts.sourceCounts,
			vector: {
				enabled: true,
				available: true
			},
			batch: {
				enabled: false,
				failures: 0,
				limit: 0,
				wait: false,
				concurrency: 0,
				pollIntervalMs: 0,
				timeoutMs: 0
			},
			custom: { qmd: {
				collections: this.qmd.collections.length,
				lastUpdateAt: this.lastUpdateAt,
				lastEmbedAt: this.lastEmbedAt,
				lastConsolidateAt: this.lastConsolidateAt,
				updateCountSinceLastConsolidate: this.updateCountSinceLastConsolidate,
				embedPending: Boolean(this.pendingEmbed),
				newTokensSinceLastEmbed: this.newTokensSinceLastEmbed,
				embedTokenThreshold: this.qmd.update.embedTokenThreshold,
				embedMinIntervalMs: this.qmd.update.embedMinIntervalMs,
				embedMaxIntervalMs: this.qmd.update.embedMaxIntervalMs,
				totalContentHashes: embeddingCoverage?.totalContentHashes,
				vectorizedContentHashes: embeddingCoverage?.vectorizedContentHashes,
				missingVectorHashes: embeddingCoverage?.missingVectorHashes,
				pool: this.pool.stats()
			} }
		};
	}
	async probeEmbeddingAvailability() {
		return { ok: true };
	}
	async probeVectorAvailability() {
		return true;
	}
	/**
	* Extract a date from a file path (e.g., "memory/2026-04-04.md" → Date).
	* Falls back to null if no date pattern found.
	*/
	extractDateFromPath(relPath) {
		const match = relPath.match(/(\d{4}-\d{2}-\d{2})/);
		if (!match) return null;
		const parsed = /* @__PURE__ */ new Date(match[1] + "T00:00:00");
		return Number.isNaN(parsed.getTime()) ? null : parsed;
	}
	async flushIndex(reason) {
		if (this.sessionExporter) await this.exportSessions();
		await this.runQmd(["update"], { timeoutMs: 12e4 });
		this.lastUpdateAt = Date.now();
		this.docPathCache.clear();
		this.notifyIndexChanged();
		log.info(`qmd flush-index complete (${reason})`);
	}
	onIndexChanged(callback) {
		this.indexChangedCallbacks.push(callback);
	}
	notifyIndexChanged() {
		for (const cb of this.indexChangedCallbacks) try {
			cb();
		} catch {}
	}
	async close() {
		if (this.closed) return;
		this.closed = true;
		if (this.updateTimer) {
			clearInterval(this.updateTimer);
			this.updateTimer = null;
		}
		if (this.embedTimer) {
			clearInterval(this.embedTimer);
			this.embedTimer = null;
		}
		if (this.consolidateTimer) {
			clearInterval(this.consolidateTimer);
			this.consolidateTimer = null;
		}
		await this.pendingUpdate?.catch(() => void 0);
		await this.pendingEmbed?.catch(() => void 0);
		await this.pendingConsolidate?.catch(() => void 0);
		await this.pool.close();
		if (this.db) {
			this.db.close();
			this.db = null;
		}
	}
	async runUpdate(reason, force) {
		if (this.pendingUpdate && !force) return this.pendingUpdate;
		if (this.shouldSkipUpdate(force)) return;
		const run = async () => {
			if (this.sessionExporter) await this.exportSessions();
			await this.runQmd(["update"], { timeoutMs: 12e4 });
			this.lastUpdateAt = Date.now();
			this.updateCountSinceLastConsolidate += 1;
			this.docPathCache.clear();
			this.notifyIndexChanged();
			this.maybeScheduleEmbed(reason);
		};
		this.pendingUpdate = run().finally(() => {
			this.pendingUpdate = null;
		});
		await this.pendingUpdate;
	}
	maybeScheduleEmbed(reason) {
		if (this.pendingEmbed || this.closed) return;
		const { embedMinIntervalMs, embedMaxIntervalMs, embedTokenThreshold } = this.qmd.update;
		const elapsed = this.lastEmbedAt !== null ? Date.now() - this.lastEmbedAt : Infinity;
		if (elapsed < embedMinIntervalMs) return;
		const tokenThresholdMet = this.newTokensSinceLastEmbed >= embedTokenThreshold;
		const maxIntervalMet = elapsed >= embedMaxIntervalMs;
		const firstRun = this.lastEmbedAt === null;
		if (!tokenThresholdMet && !maxIntervalMet && !firstRun) return;
		log.info(`qmd embed scheduled (${reason}, tokens=${this.newTokensSinceLastEmbed}, elapsed=${Math.round(elapsed / 1e3)}s)`);
		this.runEmbedAsync(reason);
	}
	runEmbedAsync(reason) {
		if (this.pendingEmbed) return;
		const run = async () => {
			await this.runQmd(["embed"], { timeoutMs: 12e4 });
			this.lastEmbedAt = Date.now();
			this.newTokensSinceLastEmbed = 0;
		};
		this.pendingEmbed = run().catch((err) => {
			log.warn(`qmd embed failed (${reason}): ${String(err)}`);
		}).finally(() => {
			this.pendingEmbed = null;
		});
	}
	trackNewTokens(count) {
		this.newTokensSinceLastEmbed += count;
	}
	async runConsolidation(force) {
		if (this.pendingConsolidate && !force) return this.pendingConsolidate;
		if (this.shouldSkipConsolidate(force)) return;
		const run = async () => {
			await this.runQmd(["consolidate"], { timeoutMs: 12e4 });
			this.lastConsolidateAt = Date.now();
			this.updateCountSinceLastConsolidate = 0;
		};
		this.pendingConsolidate = run().finally(() => {
			this.pendingConsolidate = null;
		});
		await this.pendingConsolidate;
	}
	async runQmd(args, opts) {
		return this.pool.exec(args, {
			env: this.env,
			cwd: this.workspaceDir,
			timeoutMs: opts?.timeoutMs
		});
	}
	ensureDb() {
		if (this.db) return this.db;
		const { DatabaseSync } = requireNodeSqlite();
		this.db = new DatabaseSync(this.indexPath, { readOnly: true });
		return this.db;
	}
	async exportSessions() {
		if (!this.sessionExporter) return;
		const exportDir = this.sessionExporter.dir;
		await fs.mkdir(exportDir, { recursive: true });
		const files = await listSessionFilesForAgent(this.agentId);
		const keep = /* @__PURE__ */ new Set();
		const cutoff = this.sessionExporter.retentionMs ? Date.now() - this.sessionExporter.retentionMs : null;
		for (const sessionFile of files) {
			const entry = await buildSessionEntry(sessionFile);
			if (!entry) continue;
			if (cutoff && entry.mtimeMs < cutoff) continue;
			const target = path.join(exportDir, `${path.basename(sessionFile, ".jsonl")}.md`);
			await fs.writeFile(target, this.renderSessionMarkdown(entry), "utf-8");
			keep.add(target);
		}
		const exported = await fs.readdir(exportDir).catch(() => []);
		for (const name of exported) {
			if (!name.endsWith(".md")) continue;
			const full = path.join(exportDir, name);
			if (!keep.has(full)) await fs.rm(full, { force: true });
		}
	}
	renderSessionMarkdown(entry) {
		return `${`# Session ${path.basename(entry.absPath, path.extname(entry.absPath))}`}\n\n${entry.content?.trim().length ? entry.content.trim() : "(empty)"}\n`;
	}
	pickSessionCollectionName() {
		const existing = new Set(this.qmd.collections.map((collection) => collection.name));
		if (!existing.has("sessions")) return "sessions";
		let counter = 2;
		let candidate = `sessions-${counter}`;
		while (existing.has(candidate)) {
			counter += 1;
			candidate = `sessions-${counter}`;
		}
		return candidate;
	}
	async resolveDocLocation(docid) {
		if (!docid) return null;
		const normalized = docid.startsWith("#") ? docid.slice(1) : docid;
		if (!normalized) return null;
		const cached = this.docPathCache.get(normalized);
		if (cached) return cached;
		const row = this.ensureDb().prepare("SELECT d.collection, d.path, c.memory_tier, c.access_count, c.event_time FROM documents d LEFT JOIN chunks c ON c.path = d.path AND c.active = 1 WHERE d.hash LIKE ? AND d.active = 1 LIMIT 1").get(`${normalized}%`);
		if (!row) return null;
		const location = this.toDocLocation(row.collection, row.path);
		if (!location) return null;
		const enriched = {
			...location,
			memoryTier: row.memory_tier ?? void 0,
			accessCount: typeof row.access_count === "number" ? row.access_count : void 0,
			eventTimeMs: typeof row.event_time === "number" ? row.event_time : void 0
		};
		this.docPathCache.set(normalized, enriched);
		return enriched;
	}
	/**
	* Increment access count for retrieved search results (fire-and-forget).
	*/
	updateAccessCounts(paths) {
		if (paths.length === 0) return;
		try {
			const db = this.ensureDb();
			const now = Date.now();
			const stmt = db.prepare("UPDATE chunks SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE path = ? AND active = 1");
			for (const p of paths) try {
				stmt.run(now, p);
			} catch {}
		} catch {}
	}
	extractSnippetLines(snippet) {
		const match = SNIPPET_HEADER_RE.exec(snippet);
		if (match) {
			const start = Number(match[1]);
			const count = Number(match[2]);
			if (Number.isFinite(start) && Number.isFinite(count)) return {
				startLine: start,
				endLine: start + count - 1
			};
		}
		return {
			startLine: 1,
			endLine: snippet.split("\n").length
		};
	}
	readEmbeddingCoverage() {
		try {
			const db = this.ensureDb();
			const totalContentHashes = Number(db.prepare("SELECT COUNT(DISTINCT hash) as c FROM documents WHERE active = 1").get()?.c ?? 0);
			const vectorizedContentHashes = Number(db.prepare(`SELECT COUNT(DISTINCT d.hash) as c
             FROM documents d
             JOIN content_vectors v ON v.hash = d.hash
             WHERE d.active = 1`).get()?.c ?? 0);
			return {
				totalContentHashes,
				vectorizedContentHashes,
				missingVectorHashes: Math.max(0, totalContentHashes - vectorizedContentHashes)
			};
		} catch (err) {
			log.warn(`failed to read qmd embedding coverage: ${String(err)}`);
			return null;
		}
	}
	readCounts() {
		try {
			const rows = this.ensureDb().prepare("SELECT collection, COUNT(*) as c FROM documents WHERE active = 1 GROUP BY collection").all();
			const bySource = /* @__PURE__ */ new Map();
			for (const source of this.sources) bySource.set(source, {
				files: 0,
				chunks: 0
			});
			let total = 0;
			for (const row of rows) {
				const source = this.collectionRoots.get(row.collection)?.kind ?? "memory";
				const entry = bySource.get(source) ?? {
					files: 0,
					chunks: 0
				};
				entry.files += row.c ?? 0;
				entry.chunks += row.c ?? 0;
				bySource.set(source, entry);
				total += row.c ?? 0;
			}
			return {
				totalDocuments: total,
				sourceCounts: Array.from(bySource.entries()).map(([source, value]) => ({
					source,
					files: value.files,
					chunks: value.chunks
				}))
			};
		} catch (err) {
			log.warn(`failed to read qmd index stats: ${String(err)}`);
			return {
				totalDocuments: 0,
				sourceCounts: Array.from(this.sources).map((source) => ({
					source,
					files: 0,
					chunks: 0
				}))
			};
		}
	}
	isDocumentAllowed(relPath, accessScope) {
		return isMemoryPathAllowed({
			relPath,
			workspaceDir: this.workspaceDir,
			collections: this.qmd.collections,
			allowedCollections: accessScope?.allowedCollections
		});
	}
	isScopeAllowed(sessionKey) {
		const scope = this.qmd.scope;
		if (!scope) return true;
		const channel = this.deriveChannelFromKey(sessionKey);
		const chatType = this.deriveChatTypeFromKey(sessionKey);
		const normalizedKey = sessionKey ?? "";
		for (const rule of scope.rules ?? []) {
			if (!rule) continue;
			const match = rule.match ?? {};
			if (match.channel && match.channel !== channel) continue;
			if (match.chatType && match.chatType !== chatType) continue;
			if (match.keyPrefix && !normalizedKey.startsWith(match.keyPrefix)) continue;
			return rule.action === "allow";
		}
		return (scope.default ?? "allow") === "allow";
	}
	deriveChannelFromKey(key) {
		if (!key) return;
		const normalized = this.normalizeSessionKey(key);
		if (!normalized) return;
		const parts = normalized.split(":").filter(Boolean);
		if (parts.length >= 2 && (parts[1] === "group" || parts[1] === "channel" || parts[1] === "dm")) return parts[0]?.toLowerCase();
	}
	deriveChatTypeFromKey(key) {
		if (!key) return;
		const normalized = this.normalizeSessionKey(key);
		if (!normalized) return;
		if (normalized.includes(":group:")) return "group";
		if (normalized.includes(":channel:")) return "channel";
		return "direct";
	}
	normalizeSessionKey(key) {
		const trimmed = key.trim();
		if (!trimmed) return;
		const normalized = (parseAgentSessionKey(trimmed)?.rest ?? trimmed).toLowerCase();
		if (normalized.startsWith("subagent:")) return;
		return normalized;
	}
	toDocLocation(collection, collectionRelativePath) {
		const root = this.collectionRoots.get(collection);
		if (!root) return null;
		const normalizedRelative = collectionRelativePath.replace(/\\/g, "/");
		const absPath = path.normalize(path.resolve(root.path, collectionRelativePath));
		const relativeToWorkspace = path.relative(this.workspaceDir, absPath);
		return {
			rel: this.buildSearchPath(collection, normalizedRelative, relativeToWorkspace, absPath),
			abs: absPath,
			source: root.kind
		};
	}
	buildSearchPath(collection, collectionRelativePath, relativeToWorkspace, absPath) {
		if (this.isInsideWorkspace(relativeToWorkspace)) {
			const normalized = relativeToWorkspace.replace(/\\/g, "/");
			if (!normalized) return path.basename(absPath);
			return normalized;
		}
		return `qmd/${collection}/${collectionRelativePath.replace(/^\/+/, "")}`;
	}
	isInsideWorkspace(relativePath) {
		if (!relativePath) return true;
		if (relativePath.startsWith("..")) return false;
		if (relativePath.startsWith(`..${path.sep}`)) return false;
		return !path.isAbsolute(relativePath);
	}
	resolveReadPath(relPath) {
		if (relPath.startsWith("qmd/")) {
			const [, collection, ...rest] = relPath.split("/");
			if (!collection || rest.length === 0) throw new Error("invalid qmd path");
			const root = this.collectionRoots.get(collection);
			if (!root) throw new Error(`unknown qmd collection: ${collection}`);
			const joined = rest.join("/");
			const resolved = path.resolve(root.path, joined);
			if (!this.isWithinRoot(root.path, resolved)) throw new Error("qmd path escapes collection");
			return resolved;
		}
		const absPath = path.resolve(this.workspaceDir, relPath);
		if (!this.isWithinWorkspace(absPath)) throw new Error("path escapes workspace");
		return absPath;
	}
	isWithinWorkspace(absPath) {
		const normalizedWorkspace = this.workspaceDir.endsWith(path.sep) ? this.workspaceDir : `${this.workspaceDir}${path.sep}`;
		if (absPath === this.workspaceDir) return true;
		return (absPath.endsWith(path.sep) ? absPath : `${absPath}${path.sep}`).startsWith(normalizedWorkspace);
	}
	isWithinRoot(root, candidate) {
		const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
		if (candidate === root) return true;
		return (candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`).startsWith(normalizedRoot);
	}
	clampResultsByInjectedChars(results) {
		const budget = this.qmd.limits.maxInjectedChars;
		if (!budget || budget <= 0) return results;
		let remaining = budget;
		const clamped = [];
		for (const entry of results) {
			if (remaining <= 0) break;
			const snippet = entry.snippet ?? "";
			if (snippet.length <= remaining) {
				clamped.push(entry);
				remaining -= snippet.length;
			} else {
				const trimmed = snippet.slice(0, Math.max(0, remaining));
				clamped.push({
					...entry,
					snippet: trimmed
				});
				break;
			}
		}
		return clamped;
	}
	shouldSkipUpdate(force) {
		if (force) return false;
		const debounceMs = this.qmd.update.debounceMs;
		if (debounceMs <= 0) return false;
		if (!this.lastUpdateAt) return false;
		return Date.now() - this.lastUpdateAt < debounceMs;
	}
	shouldConsolidate() {
		if (this.pendingConsolidate) return false;
		if (this.shouldSkipConsolidate()) return false;
		if (this.updateCountSinceLastConsolidate >= CONSOLIDATE_UPDATE_THRESHOLD) return true;
		return this.readCounts().totalDocuments >= CONSOLIDATE_FRAGMENT_THRESHOLD;
	}
	shouldSkipConsolidate(force) {
		if (force) return false;
		const debounceMs = this.qmd.consolidate.debounceMs;
		if (debounceMs <= 0) return false;
		if (!this.lastConsolidateAt) return false;
		return Date.now() - this.lastConsolidateAt < debounceMs;
	}
};

//#endregion
export { QmdMemoryManager };