import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
//#region extensions/crypto/src/services/session-recall.ts
/**
* Session Recall Service — full-text search over past conversations.
*
* Indexes conversation turns to JSONL on disk and provides in-memory
* full-text search with relevance ranking. No external dependencies
* (no SQLite — uses JSONL + in-memory search to match the codebase's
* file-based persistence pattern).
*
* How it works:
*   1. message_received and after_tool_call hooks feed messages in
*   2. Messages are appended to ~/.openclawnch/recall/sessions.jsonl
*   3. On startup, the index is loaded into memory
*   4. Search queries tokenize and match against the index
*   5. Results are grouped by session with context windows
*
* The in-memory approach works well for single-agent deployments with
* thousands of conversations. For multi-agent or very large histories,
* this can be upgraded to SQLite FTS5 later.
*
* Inspired by Hermes Agent's session_search_tool.py.
*/
const DEFAULT_MAX_ENTRY_CHARS = 2e3;
const DEFAULT_MAX_ENTRIES = 5e4;
const DEFAULT_MAX_RESULTS = 5;
function tokenize(text) {
	return text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
}
function computeRelevance(entryTokens, queryTokens) {
	let score = 0;
	const entrySet = new Set(entryTokens);
	for (const qt of queryTokens) {
		if (entrySet.has(qt)) score += 1;
		for (const et of entryTokens) if (et.startsWith(qt) && et !== qt) score += .5;
	}
	return queryTokens.length > 0 ? score / queryTokens.length : 0;
}
var SessionRecallService = class SessionRecallService {
	config;
	entries = [];
	nextSeq = 1;
	loaded = false;
	constructor(config = {}) {
		this.config = {
			baseDir: config.baseDir ?? join(process.env.HOME ?? "/tmp", ".openclawnch", "recall"),
			maxEntryChars: config.maxEntryChars ?? DEFAULT_MAX_ENTRY_CHARS,
			maxEntries: config.maxEntries ?? DEFAULT_MAX_ENTRIES,
			maxResults: config.maxResults ?? DEFAULT_MAX_RESULTS
		};
	}
	/**
	* Record a conversation turn for future recall.
	*/
	recordTurn(entry) {
		this.ensureLoaded();
		const record = {
			...entry,
			content: entry.content.slice(0, this.config.maxEntryChars),
			seq: this.nextSeq++
		};
		this.entries.push(record);
		this.appendToDisk(record);
		if (this.entries.length > this.config.maxEntries) {
			const excess = this.entries.length - this.config.maxEntries;
			this.entries.splice(0, excess);
		}
	}
	/**
	* Search past conversations for relevant context.
	* Returns sessions ranked by relevance.
	*/
	search(query, maxResults) {
		this.ensureLoaded();
		const limit = maxResults ?? this.config.maxResults;
		const queryTokens = tokenize(query);
		if (queryTokens.length === 0) return [];
		const scored = [];
		for (const entry of this.entries) {
			const score = computeRelevance(tokenize(entry.content), queryTokens);
			if (score > 0) scored.push({
				entry,
				score
			});
		}
		const sessionScores = /* @__PURE__ */ new Map();
		for (const { entry, score } of scored) {
			let session = sessionScores.get(entry.sessionKey);
			if (!session) {
				session = {
					entries: [],
					totalScore: 0
				};
				sessionScores.set(entry.sessionKey, session);
			}
			session.entries.push(entry);
			session.totalScore += score;
		}
		const results = [];
		for (const [sessionKey, session] of sessionScores) {
			session.entries.sort((a, b) => {
				const scoreA = computeRelevance(tokenize(a.content), queryTokens);
				return computeRelevance(tokenize(b.content), queryTokens) - scoreA;
			});
			const topEntries = session.entries.slice(0, 10);
			const timestamps = topEntries.map((e) => e.timestamp);
			results.push({
				sessionKey,
				matches: topEntries,
				score: session.totalScore,
				earliestTimestamp: Math.min(...timestamps),
				latestTimestamp: Math.max(...timestamps)
			});
		}
		results.sort((a, b) => b.score - a.score);
		return results.slice(0, limit);
	}
	getFilePath() {
		return join(this.config.baseDir, "sessions.jsonl");
	}
	ensureLoaded() {
		if (this.loaded) return;
		this.loaded = true;
		try {
			const filePath = this.getFilePath();
			if (!existsSync(filePath)) return;
			const lines = readFileSync(filePath, "utf8").split("\n").filter(Boolean);
			for (const line of lines) try {
				const entry = JSON.parse(line);
				this.entries.push(entry);
				if (entry.seq >= this.nextSeq) this.nextSeq = entry.seq + 1;
			} catch {}
			if (this.entries.length > this.config.maxEntries) {
				const excess = this.entries.length - this.config.maxEntries;
				this.entries.splice(0, excess);
			}
		} catch {}
	}
	diskWrites = 0;
	static COMPACT_INTERVAL = 500;
	static MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
	appendToDisk(entry) {
		try {
			const dir = this.config.baseDir;
			if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
			appendFileSync(this.getFilePath(), JSON.stringify(entry) + "\n", "utf8");
			this.diskWrites++;
			if (this.diskWrites % SessionRecallService.COMPACT_INTERVAL === 0) this.maybeCompact();
		} catch {}
	}
	/**
	* Compact the JSONL file by rewriting it with only the in-memory entries.
	* Prevents unbounded disk growth — the file is capped at maxEntries lines.
	*/
	maybeCompact() {
		try {
			const filePath = this.getFilePath();
			if (!existsSync(filePath)) return;
			if (statSync(filePath).size < SessionRecallService.MAX_FILE_SIZE_BYTES) return;
			writeFileSync(filePath, this.entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
		} catch {}
	}
	getStats() {
		this.ensureLoaded();
		const sessions = new Set(this.entries.map((e) => e.sessionKey));
		const timestamps = this.entries.map((e) => e.timestamp);
		return {
			totalEntries: this.entries.length,
			uniqueSessions: sessions.size,
			oldestTimestamp: timestamps.length > 0 ? Math.min(...timestamps) : 0,
			newestTimestamp: timestamps.length > 0 ? Math.max(...timestamps) : 0
		};
	}
};
let _instance = null;
function getSessionRecall(config) {
	if (!_instance) _instance = new SessionRecallService(config);
	return _instance;
}
function resetSessionRecall() {
	_instance = null;
}
//#endregion
export { getSessionRecall, resetSessionRecall };

//# sourceMappingURL=session-recall.mjs.map