import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_agent_scope, listAgentEntries, resolveDefaultAgentId } from "../agent/agent-scope.js";
import { init_artifacts, parseCompactionCheckpointTranscriptFileName } from "./parity/artifacts.js";
import { init_session_id, validateSessionId } from "./parity/session-id.js";
import { archiveFileOnDisk, init_transcript_paths, resolveSessionFilePath, resolveSessionTranscriptPathInDir } from "./parity/transcript-paths.js";
import { FILENAMES, init_paths, resolveSessionsDir } from "../config/paths.js";
import { init_session_key, parseSessionKey } from "../routing/session-key.js";
import { resolveEffectiveAgentProfileForSession } from "../config/agent-profile.js";
import { loadEntriesFromFile } from "./parity/load-jsonl-entries.js";
import { mergeLlmMessagesPreservingContextRows } from "./session-context-for-llm.js";
import { appendPiTranscriptContextEntry, persistMergedTranscriptRows, readTranscriptRowsFromFile, rowsToLlmMessages, writeTranscriptJsonl } from "./parity/jsonl-transcript-io.js";
import { invalidateSessionSearchIndexCache } from "./search-index-cache.js";
import { readPostCompactionContext } from "../agent/reply/post-compaction-context.js";
import { SessionCompactor } from "../agent/memory/compaction.js";
import { SlidingWindow } from "../agent/memory/window.js";
import { normalizeCompactionCheckpointId } from "./compaction-checkpoints.js";
import "./types.js";
import { readSessionsJsonFile, withSessionsJsonLock } from "./parity/sessions-json-file.js";
import { buildSessionsJsonStatsPatch, incrementSessionsJsonStatsForAppend, isAppendOnlyLlmTranscriptMessage, patchSessionsJsonEntryStats } from "./parity/sessions-json-patch.js";
import { countTranscriptMessageRows, readDisplayMessagePageFromTranscriptFile } from "./parity/transcript-pagination.js";
import { copyFile, mkdir, readdir, stat, unlink } from "fs/promises";
import { randomUUID } from "node:crypto";
import { join } from "path";
import { existsSync } from "fs";
//#region src/session/store.ts
init_paths();
init_agent_scope();
init_session_key();
init_logger();
init_artifacts();
init_transcript_paths();
init_session_id();
const log = createLogger("SessionStore");
const INDEX_VERSION = "1.0";
const DELETED_MARKER = ".jsonl.deleted.";
var SessionStore = class {
	sessionsDir;
	archiveDir;
	storePath;
	window;
	compactor;
	storeMutationDepth = 0;
	storeMutationChain = Promise.resolve();
	/** Cache of per-agent sessions dirs to avoid re-resolution on every call. */
	agentSessionsDirCache = /* @__PURE__ */ new Map();
	constructor(options, windowConfig, compactionConfig) {
		this.options = options;
		const agentId = options.agentId ?? resolveDefaultAgentId(options.config);
		this.sessionsDir = options.sessionsDir ?? resolveSessionsDir(options.config, agentId);
		this.archiveDir = join(this.sessionsDir, "archive");
		this.storePath = join(this.sessionsDir, FILENAMES.SESSIONS_MAP);
		this.window = new SlidingWindow(windowConfig);
		this.compactor = new SessionCompactor(compactionConfig);
	}
	getSessionsRoot() {
		return this.sessionsDir;
	}
	/**
	* OpenClaw-aligned: resolve the sessions directory for a given session key.
	* Extracts agentId from the session key and routes to `agents/<agentId>/sessions/`.
	* Falls back to the default sessions directory when agentId cannot be parsed
	* or when `sessionsDir` was explicitly provided in options.
	*/
	resolveSessionsDirForKey(sessionKey) {
		if (this.options.sessionsDir) return this.sessionsDir;
		const parsed = parseSessionKey(sessionKey);
		if (!parsed) return this.sessionsDir;
		const agentId = parsed.agentId;
		const cached = this.agentSessionsDirCache.get(agentId);
		if (cached) return cached;
		const resolved = resolveSessionsDir(this.options.config, agentId);
		this.agentSessionsDirCache.set(agentId, resolved);
		return resolved;
	}
	resolveStorePathForKey(sessionKey) {
		return join(this.resolveSessionsDirForKey(sessionKey), FILENAMES.SESSIONS_MAP);
	}
	async runStoreMutation(fn) {
		if (this.storeMutationDepth > 0) return fn();
		const run = this.storeMutationChain.then(async () => {
			this.storeMutationDepth++;
			try {
				return await fn();
			} finally {
				this.storeMutationDepth--;
			}
		});
		this.storeMutationChain = run.then(() => void 0).catch(() => void 0);
		return run;
	}
	async initialize() {
		await mkdir(this.sessionsDir, { recursive: true });
		await mkdir(this.archiveDir, { recursive: true });
		if (!existsSync(this.storePath)) await withSessionsJsonLock(this.storePath, async () => void 0);
		log.debug("Session store initialized (sessions.json + JSONL)");
	}
	transcriptPathForEntry(entry, sessionsDir) {
		return resolveSessionFilePath(entry.sessionId, entry, { sessionsDir: sessionsDir ?? this.sessionsDir });
	}
	async readMapForKey(sessionKey) {
		return readSessionsJsonFile(this.resolveStorePathForKey(sessionKey));
	}
	async readMap() {
		return readSessionsJsonFile(this.storePath);
	}
	/**
	* OpenClaw-aligned: read sessions.json from ALL known agents and merge into a single map.
	* Used by aggregation queries (list, getByAgent, getByAccount, etc.) so the gateway UI
	* can display sessions across all agents.
	*/
	async readAllMaps() {
		if (this.options.sessionsDir) return this.readMap();
		const agents = listAgentEntries(this.options.config);
		const defaultId = resolveDefaultAgentId(this.options.config);
		const agentIds = new Set([defaultId, ...agents.map((a) => a.id)]);
		const merged = {};
		for (const id of agentIds) {
			const path = join(resolveSessionsDir(this.options.config, id), FILENAMES.SESSIONS_MAP);
			if (!existsSync(path)) continue;
			const map = await readSessionsJsonFile(path);
			Object.assign(merged, map);
		}
		return merged;
	}
	async getDiskEntry(sessionKey) {
		return (await this.readMapForKey(sessionKey))[sessionKey];
	}
	buildDefaultMetadata(key) {
		const { channel, chatId } = this.parseSessionKey(key);
		const routing = this.extractRoutingFromKey(key);
		const isCronSession = channel === "cron";
		const isHeartbeatSession = channel === "heartbeat";
		const now = (/* @__PURE__ */ new Date()).toISOString();
		return {
			key,
			status: "active",
			tags: [],
			createdAt: now,
			updatedAt: now,
			lastAccessedAt: now,
			messageCount: 0,
			estimatedTokens: 0,
			compactedCount: 0,
			sourceChannel: channel,
			sourceChatId: chatId,
			routing,
			...isCronSession ? {
				sessionType: "cron",
				customData: { cronJobId: chatId }
			} : {},
			...isHeartbeatSession ? {
				sessionType: "heartbeat",
				customData: { heartbeatTarget: chatId }
			} : {},
			stats: {
				messageCount: 0,
				tokenCount: 0
			}
		};
	}
	parseSessionKey(key) {
		const parts = key.split(":");
		if (parts.length >= 2 && parts[0] === "heartbeat") return {
			channel: "heartbeat",
			chatId: parts.slice(1).join(":")
		};
		const parsed = parseSessionKey(key);
		if (parsed) {
			if (parsed.source === "cron") return {
				channel: "cron",
				chatId: parsed.peerId
			};
			return {
				channel: parsed.source,
				chatId: [
					parsed.accountId,
					parsed.peerKind,
					parsed.peerId
				].join(":")
			};
		}
		return {
			channel: "unknown",
			chatId: key
		};
	}
	extractRoutingFromKey(key) {
		const parsed = parseSessionKey(key);
		if (!parsed) return;
		return {
			agentId: parsed.agentId?.toLowerCase() || "main",
			source: parsed.source?.toLowerCase() || "unknown",
			accountId: parsed.accountId?.toLowerCase() || "default",
			peerKind: parsed.peerKind?.toLowerCase() || "dm",
			peerId: parsed.peerId?.toLowerCase() || "unknown",
			threadId: parsed.threadId,
			scopeId: parsed.scopeId
		};
	}
	/** Resolve on-disk transcript path; creates session row + empty JSONL when missing. */
	async resolveTranscriptPath(sessionKey) {
		const entry = await this.ensureSession(sessionKey);
		const sessionsDir = this.resolveSessionsDirForKey(sessionKey);
		const absPath = this.transcriptPathForEntry(entry, sessionsDir);
		return {
			sessionId: entry.sessionId,
			absPath,
			sessionsDir
		};
	}
	/** Ensure sessions.json has an entry and transcript file exist for `sessionKey`. */
	async ensureSession(sessionKey) {
		const keyStorePath = this.resolveStorePathForKey(sessionKey);
		const keySessionsDir = this.resolveSessionsDirForKey(sessionKey);
		await mkdir(keySessionsDir, { recursive: true });
		return withSessionsJsonLock(keyStorePath, async (map) => {
			const existing = map[sessionKey];
			if (existing?.pluginExtensions?.xopc?.metadata) return existing;
			let entry = existing;
			if (!entry) {
				const sessionId = randomUUID();
				validateSessionId(sessionId);
				const sessionFile = `${sessionId}.jsonl`;
				const now = Date.now();
				const metadata = this.buildDefaultMetadata(sessionKey);
				metadata.transcriptId = sessionId;
				entry = {
					sessionId,
					updatedAt: now,
					sessionStartedAt: now,
					sessionFile,
					pluginExtensions: { xopc: { metadata } }
				};
				map[sessionKey] = entry;
				await writeTranscriptJsonl({
					absPath: resolveSessionTranscriptPathInDir(sessionId, keySessionsDir),
					sessionId,
					cwd: process.cwd(),
					rows: []
				});
			} else if (!entry.pluginExtensions?.xopc?.metadata) {
				const metadata = this.buildDefaultMetadata(sessionKey);
				metadata.transcriptId = entry.sessionId;
				entry.pluginExtensions = { xopc: { metadata } };
				map[sessionKey] = entry;
			}
			return entry;
		});
	}
	metadataFromEntry(sessionKey, entry) {
		const base = entry.pluginExtensions?.xopc?.metadata ?? this.buildDefaultMetadata(sessionKey);
		const { channel: keySource, chatId: keyChatId } = this.parseSessionKey(sessionKey);
		const diskSc = typeof base.sourceChannel === "string" ? base.sourceChannel.trim() : "";
		const diskChat = typeof base.sourceChatId === "string" ? base.sourceChatId.trim() : "";
		return {
			...base,
			key: sessionKey,
			transcriptId: entry.sessionId,
			sourceChannel: diskSc || keySource,
			sourceChatId: diskChat || keyChatId
		};
	}
	async getByAgent(agentId) {
		const map = await this.readAllMaps();
		const out = [];
		for (const [key, e] of Object.entries(map)) {
			const m = this.metadataFromEntry(key, e);
			if (m.routing?.agentId?.toLowerCase() === agentId.toLowerCase()) out.push(m);
		}
		return out;
	}
	async getByAccount(accountId) {
		const map = await this.readAllMaps();
		const out = [];
		for (const [key, e] of Object.entries(map)) {
			const m = this.metadataFromEntry(key, e);
			if (m.routing?.accountId === accountId) out.push(m);
		}
		return out;
	}
	async getByPeer(peerKind, peerId) {
		const map = await this.readAllMaps();
		const out = [];
		for (const [key, e] of Object.entries(map)) {
			const m = this.metadataFromEntry(key, e);
			if (m.routing?.peerKind === peerKind && m.routing.peerId === peerId) out.push(m);
		}
		return out;
	}
	async getMainSession(channel, accountId) {
		const map = await this.readAllMaps();
		for (const [key, e] of Object.entries(map)) {
			const m = this.metadataFromEntry(key, e);
			if (m.routing?.source === channel && m.routing.accountId === accountId && m.routing.peerKind === "dm" && m.routing.peerId === "main") return m;
		}
		return null;
	}
	async refreshIndex() {}
	async list(query = {}) {
		const map = await this.readAllMaps();
		let sessions = Object.entries(map).map(([k, e]) => this.metadataFromEntry(k, e));
		if (query.status) {
			const statuses = Array.isArray(query.status) ? query.status : [query.status];
			sessions = sessions.filter((s) => statuses.includes(s.status));
		}
		if (query.channel) {
			const rawChannels = query.channel.split(",").map((c) => c.trim().toLowerCase()).filter(Boolean);
			/**
			* `ui` is a legacy console source; treat as webchat when filtering web sessions.
			* `webui` matches slash-command normalization to `gateway` (see `chat-commands/session-key.ts`).
			*/
			const channels = [...new Set(rawChannels.flatMap((c) => {
				if (c === "webchat") return ["webchat", "ui"];
				if (c === "gateway") return ["gateway", "webui"];
				return [c];
			}))];
			if (channels.length === 0) sessions = [];
			else if (channels.length === 1) {
				const ch = channels[0];
				sessions = sessions.filter((s) => (s.sourceChannel ?? "").toLowerCase() === ch);
			} else sessions = sessions.filter((s) => channels.includes((s.sourceChannel ?? "").toLowerCase()));
		}
		if (query.tags?.length) sessions = sessions.filter((s) => query.tags.some((t) => s.tags.includes(t)));
		if (query.search) {
			const q = query.search.toLowerCase();
			sessions = sessions.filter((s) => s.key.toLowerCase().includes(q) || (s.name?.toLowerCase().includes(q) ?? false) || s.tags.some((t) => t.toLowerCase().includes(q)));
		}
		const sortBy = query.sortBy || "updatedAt";
		const sortOrder = query.sortOrder || "desc";
		sessions.sort((a, b) => {
			const av = a[sortBy];
			const bv = b[sortBy];
			const c = av < bv ? -1 : av > bv ? 1 : 0;
			return sortOrder === "asc" ? c : -c;
		});
		const total = sessions.length;
		const limit = query.limit || 50;
		const offset = query.offset || 0;
		return {
			items: sessions.slice(offset, offset + limit),
			total,
			limit,
			offset,
			hasMore: offset + limit < total
		};
	}
	async get(key, options) {
		const metadata = await this.getMetadata(key);
		if (!metadata) return null;
		const messages = await this.loadDisplayMessages(key);
		return await this.buildSessionDetail(key, metadata, messages, options);
	}
	async getMessagePage(key, options = {}) {
		const metadata = await this.getMetadata(key);
		if (!metadata) return null;
		const limit = Math.min(200, Math.max(1, Math.trunc(options.limit ?? 50)));
		const offset = Math.max(0, Math.trunc(options.offset ?? 0));
		const parsedBefore = options.before ? Number.parseInt(options.before, 10) : void 0;
		const hasBeforeCursor = parsedBefore !== void 0 && Number.isFinite(parsedBefore);
		if ((await this.listCompactionCheckpoints(key)).length === 0) {
			const entry = await this.getDiskEntry(key);
			if (!entry) return null;
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const page = await readDisplayMessagePageFromTranscriptFile(this.transcriptPathForEntry(entry, keySessionsDir), {
				limit,
				offset: hasBeforeCursor ? void 0 : offset,
				beforeIndex: hasBeforeCursor ? parsedBefore : void 0
			});
			const session = await this.buildSessionDetail(key, metadata, page.messages, options);
			const nextBeforeCursor = page.startIndex > 0 ? String(page.startIndex) : void 0;
			return {
				session,
				pagination: {
					total: page.total,
					limit,
					offset,
					hasMore: page.startIndex > 0,
					...hasBeforeCursor ? { before: String(page.endIndex) } : {},
					...nextBeforeCursor ? { nextBeforeCursor } : {}
				}
			};
		}
		const messages = await this.loadDisplayMessages(key);
		const total = messages.length;
		const endExclusive = hasBeforeCursor ? Math.min(total, Math.max(0, Math.trunc(parsedBefore))) : Math.max(0, total - offset);
		const startInclusive = Math.max(0, endExclusive - limit);
		const pageMessages = messages.slice(startInclusive, endExclusive);
		const session = await this.buildSessionDetail(key, metadata, pageMessages, options);
		const nextBeforeCursor = startInclusive > 0 ? String(startInclusive) : void 0;
		return {
			session,
			pagination: {
				total,
				limit,
				offset,
				hasMore: startInclusive > 0,
				...hasBeforeCursor ? { before: String(endExclusive) } : {},
				...nextBeforeCursor ? { nextBeforeCursor } : {}
			}
		};
	}
	async buildSessionDetail(key, metadata, messages, options) {
		let transcriptSummary;
		if (options?.includeTranscriptSummary) {
			const env = await this.loadTranscriptDocument(key);
			if (env) transcriptSummary = {
				id: env.id,
				version: env.version,
				createdAt: env.createdAt,
				updatedAt: env.updatedAt,
				compactionCount: env.compactions?.length ?? 0
			};
		}
		let transcriptRows;
		if (options?.includeTranscriptRows) transcriptRows = await this.loadTranscriptRows(key);
		return {
			...metadata,
			messages: this.convertMessages(messages),
			...transcriptSummary ? { transcriptSummary } : {},
			...transcriptRows !== void 0 ? { transcriptRows } : {}
		};
	}
	async loadTranscriptRows(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return [];
		return readTranscriptRowsFromFile(this.transcriptPathForEntry(entry, this.resolveSessionsDirForKey(key)));
	}
	async getMetadata(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return null;
		return this.metadataFromEntry(key, entry);
	}
	async updateMetadata(key, updates) {
		return this.runStoreMutation(async () => {
			await withSessionsJsonLock(this.resolveStorePathForKey(key), async (map) => {
				const entry = map[key];
				if (!entry?.pluginExtensions?.xopc?.metadata) throw new Error(`Session not found: ${key}`);
				const meta = {
					...entry.pluginExtensions.xopc.metadata,
					...updates,
					updatedAt: (/* @__PURE__ */ new Date()).toISOString()
				};
				entry.pluginExtensions.xopc.metadata = meta;
				entry.updatedAt = Date.now();
				map[key] = entry;
			});
			invalidateSessionSearchIndexCache();
			log.debug({
				key,
				updates
			}, "Session metadata updated");
		});
	}
	/**
	* Reset transcript for an existing session key: archive the current JSONL as
	* `*.reset.*`, assign a new `sessionId`, and preserve per-session overrides
	* on the disk entry (thinking/verbose) and in `sessions/config/*.json`.
	*/
	async reset(key) {
		return this.runStoreMutation(async () => {
			const existing = await this.getDiskEntry(key);
			if (!existing?.pluginExtensions?.xopc?.metadata) return null;
			const previousSessionId = existing.sessionId;
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const abs = this.transcriptPathForEntry(existing, keySessionsDir);
			if (existsSync(abs)) try {
				archiveFileOnDisk(abs, "reset");
			} catch (err) {
				log.warn({
					err,
					key
				}, "Transcript archive on reset failed");
			}
			const sessionId = randomUUID();
			validateSessionId(sessionId);
			const now = Date.now();
			await writeTranscriptJsonl({
				absPath: resolveSessionTranscriptPathInDir(sessionId, keySessionsDir),
				sessionId,
				cwd: process.cwd(),
				rows: []
			});
			await withSessionsJsonLock(this.resolveStorePathForKey(key), async (map) => {
				const e = map[key];
				if (!e?.pluginExtensions?.xopc?.metadata) return;
				e.sessionId = sessionId;
				e.sessionFile = `${sessionId}.jsonl`;
				e.updatedAt = now;
				e.sessionStartedAt = now;
				e.lastInteractionAt = void 0;
				patchSessionsJsonEntryStats(e, buildSessionsJsonStatsPatch(0, 0));
				const meta = e.pluginExtensions.xopc.metadata;
				meta.transcriptId = sessionId;
				meta.updatedAt = new Date(now).toISOString();
				map[key] = e;
			});
			invalidateSessionSearchIndexCache();
			log.info({
				key,
				previousSessionId,
				sessionId
			}, "Session reset");
			return {
				sessionId,
				previousSessionId
			};
		});
	}
	async delete(key) {
		return this.runStoreMutation(async () => {
			const entry = await this.getDiskEntry(key);
			if (!entry) return false;
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const abs = this.transcriptPathForEntry(entry, keySessionsDir);
			await withSessionsJsonLock(this.resolveStorePathForKey(key), async (map) => {
				delete map[key];
			});
			try {
				if (existsSync(abs)) archiveFileOnDisk(abs, "deleted");
			} catch (err) {
				log.warn({
					err,
					key
				}, "Transcript archive on delete failed");
			}
			invalidateSessionSearchIndexCache();
			log.info({ key }, "Session deleted");
			return true;
		});
	}
	async deleteMany(keys) {
		const success = [];
		const failed = [];
		for (const key of keys) try {
			if (await this.delete(key)) success.push(key);
			else failed.push(key);
		} catch {
			failed.push(key);
		}
		return {
			success,
			failed
		};
	}
	async setStatus(key, status) {
		await this.updateMetadata(key, { status });
		if (status === "archived") await this.moveToArchive(key);
		else await this.moveFromArchive(key);
	}
	async archive(key) {
		await this.setStatus(key, "archived");
	}
	async unarchive(key) {
		await this.setStatus(key, "active");
	}
	async pin(key) {
		await this.setStatus(key, "pinned");
	}
	async unpin(key) {
		await this.setStatus(key, "active");
	}
	async moveToArchive(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return;
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const abs = this.transcriptPathForEntry(entry, keySessionsDir);
		if (existsSync(abs)) try {
			archiveFileOnDisk(abs, "deleted");
		} catch (err) {
			log.warn({
				err,
				key
			}, "Archive transcript rename failed");
		}
	}
	async findMostRecentDeletedTranscript(sessionId, sessionsDir) {
		let names;
		try {
			names = await readdir(sessionsDir);
		} catch {
			return null;
		}
		const prefix = `${sessionId}${DELETED_MARKER}`;
		const hits = names.filter((n) => n.startsWith(prefix) && n.endsWith("Z"));
		hits.sort().reverse();
		const first = hits[0];
		return first ? join(sessionsDir, first) : null;
	}
	async moveFromArchive(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return;
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const target = resolveSessionTranscriptPathInDir(entry.sessionId, keySessionsDir);
		if (existsSync(target)) return;
		const src = await this.findMostRecentDeletedTranscript(entry.sessionId, keySessionsDir);
		if (!src) return;
		try {
			const { rename } = await import("fs/promises");
			await rename(src, target);
		} catch (err) {
			log.warn({
				err,
				key,
				src,
				target
			}, "Unarchive transcript rename failed");
		}
	}
	async loadMessages(key, options) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return [];
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const primary = this.transcriptPathForEntry(entry, keySessionsDir);
		if (existsSync(primary)) return rowsToLlmMessages(await readTranscriptRowsFromFile(primary));
		if (options?.fromArchive) {
			const archived = await this.findMostRecentDeletedTranscript(entry.sessionId, keySessionsDir);
			if (!archived) return [];
			return rowsToLlmMessages(await readTranscriptRowsFromFile(archived));
		}
		return [];
	}
	async loadDisplayMessages(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return [];
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const primary = this.transcriptPathForEntry(entry, keySessionsDir);
		const transcriptPaths = [];
		const checkpoints = await this.listCompactionCheckpoints(key);
		for (const checkpoint of [...checkpoints].reverse()) transcriptPaths.push(join(keySessionsDir, `${this.checkpointBasename(entry.sessionId)}${checkpoint.id}.jsonl`));
		transcriptPaths.push(primary);
		const messages = [];
		const seenMessages = /* @__PURE__ */ new Set();
		for (const transcriptPath of transcriptPaths) {
			if (!existsSync(transcriptPath)) continue;
			const rows = await readTranscriptRowsFromFile(transcriptPath);
			for (const message of rowsToLlmMessages(rows)) {
				if (this.isCompactionSummaryMessage(message)) continue;
				const key = this.displayMessageIdentity(message);
				if (seenMessages.has(key)) continue;
				seenMessages.add(key);
				messages.push(message);
			}
		}
		return messages;
	}
	async loadTranscriptDocument(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return null;
		const path = this.transcriptPathForEntry(entry, this.resolveSessionsDirForKey(key));
		if (!existsSync(path)) return null;
		const entries = loadEntriesFromFile(path);
		const header = entries.find((e) => e.type === "session");
		if (!header || typeof header.id !== "string") return null;
		const sessionHeader = header;
		const rows = await readTranscriptRowsFromFile(path);
		const compactions = entries.filter((e) => e.type === "compaction").map((c) => ({
			at: c.timestamp,
			summary: c.summary,
			firstKeptIndex: Number.parseInt(String(c.firstKeptEntryId), 10) || 0,
			tokensBefore: c.tokensBefore,
			tokensAfter: typeof c.details === "object" && c.details && "tokensAfter" in c.details && typeof c.details.tokensAfter === "number" ? c.details.tokensAfter : 0
		}));
		return {
			type: "xopc_session_transcript",
			version: 1,
			id: sessionHeader.id,
			createdAt: sessionHeader.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
			updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
			messages: rows,
			...compactions.length > 0 ? { compactions } : {}
		};
	}
	async writeTranscriptAndSyncIndex(key, rows, opts) {
		const entry = await this.ensureSession(key);
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const abs = this.transcriptPathForEntry(entry, keySessionsDir);
		const llm = rowsToLlmMessages(rows);
		await persistMergedTranscriptRows({
			absPath: abs,
			sessionId: entry.sessionId,
			cwd: process.cwd(),
			rows,
			appendCompaction: opts?.appendCompaction
		});
		await withSessionsJsonLock(this.resolveStorePathForKey(key), async (map) => {
			const e = map[key];
			if (!e?.pluginExtensions?.xopc?.metadata) return;
			patchSessionsJsonEntryStats(e, buildSessionsJsonStatsPatch(llm.length, this.estimateTokens(llm)));
			map[key] = e;
		});
		invalidateSessionSearchIndexCache();
	}
	/** Incremental sessions.json stats after guard append (OpenClaw transcript-events). */
	async syncSessionsJsonFromTranscriptUpdate(update) {
		const sessionKey = update.sessionKey?.trim();
		if (!sessionKey || !existsSync(update.sessionFile)) return;
		return this.runStoreMutation(async () => {
			await withSessionsJsonLock(this.resolveStorePathForKey(sessionKey), async (map) => {
				const e = map[sessionKey];
				if (!e?.pluginExtensions?.xopc?.metadata) return;
				if (update.message && isAppendOnlyLlmTranscriptMessage(update.message)) {
					incrementSessionsJsonStatsForAppend(e);
					map[sessionKey] = e;
					return;
				}
				const messageCount = await countTranscriptMessageRows(update.sessionFile);
				const llm = rowsToLlmMessages(await readTranscriptRowsFromFile(update.sessionFile));
				patchSessionsJsonEntryStats(e, buildSessionsJsonStatsPatch(messageCount, this.estimateTokens(llm)));
				map[sessionKey] = e;
			});
			invalidateSessionSearchIndexCache();
		});
	}
	async appendTranscriptContextEntry(key, entry) {
		return this.runStoreMutation(async () => {
			await this.ensureSession(key);
			const disk = await this.getDiskEntry(key);
			if (!disk) return;
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const absPath = this.transcriptPathForEntry(disk, keySessionsDir);
			const row = {
				kind: "context",
				id: typeof entry.id === "string" ? entry.id : void 0,
				text: typeof entry.text === "string" ? entry.text : void 0,
				data: entry.data,
				createdAt: entry.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
			};
			await appendPiTranscriptContextEntry({
				absPath,
				cwd: process.cwd(),
				entry: row,
				sessionKey: key
			});
			const llm = rowsToLlmMessages(existsSync(absPath) ? await readTranscriptRowsFromFile(absPath) : []);
			await withSessionsJsonLock(this.resolveStorePathForKey(key), async (map) => {
				const e = map[key];
				if (!e?.pluginExtensions?.xopc?.metadata) return;
				patchSessionsJsonEntryStats(e, buildSessionsJsonStatsPatch(llm.length, this.estimateTokens(llm)));
				map[key] = e;
			});
			invalidateSessionSearchIndexCache();
		});
	}
	/**
	* Bulk write entry point used by compaction, tests, and admin tools.
	* Runtime agent turns must persist via {@link guardSessionManager} + appendMessage.
	*/
	async saveMessages(key, messages) {
		return this.runStoreMutation(async () => {
			await this.ensureSession(key);
			const merged = mergeLlmMessagesPreservingContextRows(await this.loadTranscriptRows(key), messages);
			await this.writeTranscriptAndSyncIndex(key, merged);
		});
	}
	getWindowStats(messages) {
		return this.window.getStats(messages);
	}
	needsCompaction(key, messages, contextWindow) {
		return this.compactor.needsCompaction(messages, contextWindow);
	}
	prepareCompaction(key, messages, contextWindow) {
		const result = this.compactor.needsCompaction(messages, contextWindow);
		return {
			needsCompaction: result.needed,
			messages,
			stats: result
		};
	}
	checkpointBasename(sessionId) {
		return `${sessionId}.checkpoint.`;
	}
	async pruneCompactionCheckpoints(sessionId, sessionsDir) {
		const MAX = 15;
		const prefix = this.checkpointBasename(sessionId);
		let names;
		try {
			names = await readdir(sessionsDir);
		} catch {
			return;
		}
		const candidates = names.filter((n) => n.startsWith(prefix) && n.endsWith(".jsonl"));
		if (candidates.length <= MAX) return;
		const stats = await Promise.all(candidates.map(async (name) => {
			const p = join(sessionsDir, name);
			try {
				return {
					p,
					mtimeMs: (await stat(p)).mtimeMs
				};
			} catch {
				return {
					p: join(sessionsDir, name),
					mtimeMs: 0
				};
			}
		}));
		stats.sort((a, b) => a.mtimeMs - b.mtimeMs);
		for (let i = 0; i < stats.length - MAX; i++) try {
			await unlink(stats[i].p);
		} catch {}
	}
	async captureCompactionCheckpoint(sessionId, transcriptAbs, sessionsDir) {
		if (!existsSync(transcriptAbs)) return;
		const dest = join(sessionsDir, `${sessionId}.checkpoint.${randomUUID()}.jsonl`);
		try {
			await copyFile(transcriptAbs, dest);
			await this.pruneCompactionCheckpoints(sessionId, sessionsDir);
		} catch (err) {
			log.warn({
				err,
				sessionId
			}, "Compaction checkpoint copy failed");
		}
	}
	async applyCompaction(key, messages, result) {
		const compacted = this.compactor.applyCompaction(messages, result);
		return this.runStoreMutation(async () => {
			const entry = await this.getDiskEntry(key);
			if (!entry) return compacted;
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const abs = this.transcriptPathForEntry(entry, keySessionsDir);
			await this.captureCompactionCheckpoint(entry.sessionId, abs, keySessionsDir);
			const merged = mergeLlmMessagesPreservingContextRows(await this.loadTranscriptRows(key), compacted);
			await this.writeTranscriptAndSyncIndex(key, merged, { appendCompaction: {
				at: (/* @__PURE__ */ new Date()).toISOString(),
				summary: result.summary,
				firstKeptIndex: result.firstKeptIndex,
				tokensBefore: result.tokensBefore,
				tokensAfter: result.tokensAfter
			} });
			const metadata = await this.getMetadata(key);
			if (metadata) await this.updateMetadata(key, { compactedCount: metadata.compactedCount + 1 });
			log.info({
				key,
				tokensBefore: result.tokensBefore,
				tokensAfter: result.tokensAfter,
				keptMessages: compacted.length
			}, "Session compacted");
			await this.injectPostCompactionContext(key);
			return compacted;
		});
	}
	async compact(key, messages, contextWindow, instructions, force) {
		const result = await this.compactor.compact(messages, instructions, force);
		if (result.compacted) await this.applyCompaction(key, messages, result);
		return result;
	}
	async getCompactionStats(key) {
		const metadata = await this.getMetadata(key);
		if (!metadata) return;
		return {
			compactionCount: metadata.compactedCount,
			totalTokensBefore: 0,
			totalTokensAfter: 0,
			lastCompactionAt: void 0
		};
	}
	async listCompactionCheckpoints(key) {
		const entry = await this.getDiskEntry(key);
		if (!entry) return [];
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const sessionId = entry.sessionId;
		const prefix = this.checkpointBasename(sessionId);
		let names;
		try {
			names = await readdir(keySessionsDir);
		} catch {
			return [];
		}
		const files = names.filter((n) => n.startsWith(prefix) && n.endsWith(".jsonl"));
		const valid = (await Promise.all(files.map(async (name) => {
			const p = join(keySessionsDir, name);
			const id = parseCompactionCheckpointTranscriptFileName(name)?.checkpointId;
			if (!id || !normalizeCompactionCheckpointId(id)) return null;
			try {
				const s = await stat(p);
				return {
					id: normalizeCompactionCheckpointId(id),
					sizeBytes: s.size,
					modifiedAt: new Date(s.mtimeMs).toISOString()
				};
			} catch {
				return null;
			}
		}))).filter((r) => r !== null);
		valid.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
		return valid;
	}
	async getCompactionCheckpointDetail(key, checkpointId) {
		const id = normalizeCompactionCheckpointId(checkpointId);
		if (!id) return null;
		const entry = await this.getDiskEntry(key);
		if (!entry) return null;
		const cpPath = join(this.resolveSessionsDirForKey(key), `${this.checkpointBasename(entry.sessionId)}${id}.jsonl`);
		if (!existsSync(cpPath)) return null;
		try {
			const llm = rowsToLlmMessages(await readTranscriptRowsFromFile(cpPath));
			const s = await stat(cpPath);
			return {
				id,
				sizeBytes: s.size,
				modifiedAt: new Date(s.mtimeMs).toISOString(),
				messageCount: llm.length
			};
		} catch {
			return null;
		}
	}
	async restoreCompactionCheckpoint(key, checkpointId) {
		const id = normalizeCompactionCheckpointId(checkpointId);
		if (!id) throw new Error("Invalid checkpoint id");
		return this.runStoreMutation(async () => {
			const entry = await this.getDiskEntry(key);
			if (!entry) throw new Error(`Session not found: ${key}`);
			const keySessionsDir = this.resolveSessionsDirForKey(key);
			const cpPath = join(keySessionsDir, `${this.checkpointBasename(entry.sessionId)}${id}.jsonl`);
			if (!existsSync(cpPath)) throw new Error(`Checkpoint not found: ${id}`);
			await copyFile(cpPath, this.transcriptPathForEntry(entry, keySessionsDir));
			const messages = await this.loadMessages(key);
			await this.saveMessages(key, messages);
			log.info({
				key,
				checkpointId: id
			}, "Session transcript restored from compaction checkpoint");
		});
	}
	async deleteSession(key) {
		return this.delete(key);
	}
	async load(key, options) {
		return this.loadMessages(key, options);
	}
	async estimateTokenUsage(_key, messages) {
		return this.estimateTokens(messages);
	}
	async searchInSession(key, keyword) {
		const messages = await this.loadDisplayMessages(key);
		const keywordLower = keyword.toLowerCase();
		return this.convertMessages(messages.filter((m) => {
			return this.extractTextContent(this.messageContent(m)).toLowerCase().includes(keywordLower);
		}));
	}
	async exportSession(key, format) {
		const detail = await this.get(key);
		if (!detail) throw new Error(`Session not found: ${key}`);
		if (format === "json") {
			const transcriptRows = await this.loadTranscriptRows(key);
			const exportData = {
				version: INDEX_VERSION,
				exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
				metadata: detail,
				messages: detail.messages,
				transcriptRows
			};
			return JSON.stringify(exportData, null, 2);
		}
		const lines = [
			`# ${detail.name || detail.key}`,
			"",
			`- **Channel:** ${detail.sourceChannel}`,
			`- **Created:** ${detail.createdAt}`,
			`- **Messages:** ${detail.messageCount}`,
			`- **Tags:** ${detail.tags.join(", ") || "none"}`,
			"",
			"---",
			""
		];
		for (const msg of detail.messages) {
			const role = msg.role === "assistant" ? "Assistant" : msg.role === "user" ? "User" : msg.role;
			lines.push(`## ${role}`, "");
			const body = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content, null, 2);
			lines.push(body, "", "---", "");
		}
		return lines.join("\n");
	}
	async getStats() {
		const sessions = (await this.list({ limit: 1e5 })).items;
		const byChannel = {};
		for (const s of sessions) byChannel[s.sourceChannel] = (byChannel[s.sourceChannel] || 0) + 1;
		let oldestSession;
		let newestSession;
		if (sessions.length > 0) {
			const sorted = [...sessions].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
			oldestSession = sorted[0].createdAt;
			newestSession = sorted[sorted.length - 1].createdAt;
		}
		return {
			totalSessions: sessions.length,
			activeSessions: sessions.filter((s) => s.status === "active" || s.status === "idle").length,
			archivedSessions: sessions.filter((s) => s.status === "archived").length,
			pinnedSessions: sessions.filter((s) => s.status === "pinned").length,
			totalMessages: sessions.reduce((sum, s) => sum + s.messageCount, 0),
			totalTokens: sessions.reduce((sum, s) => sum + s.estimatedTokens, 0),
			oldestSession,
			newestSession,
			byChannel
		};
	}
	async archiveOld(olderThanDays) {
		const cutoff = /* @__PURE__ */ new Date();
		cutoff.setDate(cutoff.getDate() - olderThanDays);
		const list = await this.list({ limit: 1e5 });
		let archived = 0;
		for (const session of list.items) if (session.status !== "archived" && session.status !== "pinned") {
			if (new Date(session.lastAccessedAt) < cutoff) {
				await this.archive(session.key);
				archived++;
			}
		}
		return archived;
	}
	estimateTokens(messages) {
		let total = 0;
		for (const msg of messages) total += Math.ceil(this.extractTextContent(this.messageContent(msg)).length / 4);
		return total;
	}
	messageContent(msg) {
		return msg.content;
	}
	isCompactionSummaryMessage(msg) {
		if (msg.role !== "user") return false;
		const text = this.extractTextContent(this.messageContent(msg)).trim();
		return /^\[Previous conversation summary\]/i.test(text);
	}
	displayMessageIdentity(message) {
		const record = message;
		return JSON.stringify({
			role: message.role,
			timestamp: record.timestamp,
			toolCallId: record.toolCallId ?? record.tool_call_id,
			toolName: record.toolName,
			content: this.messageContent(message)
		});
	}
	extractTextContent(content) {
		if (typeof content === "string") return content;
		if (Array.isArray(content)) {
			const parts = [];
			for (const item of content) {
				if (typeof item !== "object" || item === null || !("type" in item)) continue;
				const c = item;
				if (c.type === "text" && typeof c.text === "string") parts.push(c.text);
				else if (c.type === "toolCall" || c.type === "tool_use") parts.push(c.name ? `[${c.name}]` : "");
			}
			return parts.join("");
		}
		return "";
	}
	async injectPostCompactionContext(key) {
		const contextText = readPostCompactionContext({
			cfg: this.options.config,
			sessionKey: key
		});
		if (!contextText?.trim()) return;
		const entry = await this.getDiskEntry(key);
		if (!entry) return;
		const keySessionsDir = this.resolveSessionsDirForKey(key);
		const abs = this.transcriptPathForEntry(entry, keySessionsDir);
		const workspaceDir = resolveEffectiveAgentProfileForSession(this.options.config, key).resolvedWorkspacePath;
		try {
			await appendPiTranscriptContextEntry({
				absPath: abs,
				cwd: workspaceDir,
				sessionKey: key,
				entry: {
					kind: "context",
					id: `post-compaction-${Date.now()}`,
					text: contextText,
					createdAt: (/* @__PURE__ */ new Date()).toISOString()
				}
			});
		} catch (err) {
			log.warn({
				err,
				key
			}, "Post-compaction context injection failed");
		}
	}
	convertMessages(messages) {
		return messages.map((m) => {
			const c = this.messageContent(m);
			const content = typeof c === "string" ? c : Array.isArray(c) ? c : this.extractTextContent(c);
			const row = {
				role: m.role,
				content,
				timestamp: m.timestamp ? new Date(m.timestamp).toISOString() : void 0,
				tool_call_id: m.tool_call_id || m.toolCallId,
				tool_calls: m.tool_calls,
				name: m.name
			};
			if (Array.isArray(m.attachments) && m.attachments.length > 0) row.attachments = m.attachments;
			const rawUsage = m.usage;
			if (rawUsage && typeof rawUsage === "object") {
				const inputTokens = typeof rawUsage.input === "number" ? rawUsage.input : void 0;
				const outputTokens = typeof rawUsage.output === "number" ? rawUsage.output : void 0;
				const totalTokens = typeof rawUsage.totalTokens === "number" ? rawUsage.totalTokens : typeof rawUsage.total === "number" ? rawUsage.total : void 0;
				if (inputTokens != null || outputTokens != null || totalTokens != null) row.usage = {
					inputTokens,
					outputTokens,
					totalTokens
				};
			}
			return row;
		});
	}
};
//#endregion
export { SessionStore };

//# sourceMappingURL=store.js.map