import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, open, readFile, rm, stat } from "node:fs/promises";
import path from "node:path";
//#region src/session/parity/transcript-file-lock.ts
/**
* Cross-process (and re-entrant same-process) advisory lock for session transcript JSONL.
* Aligns with the goal of safe concurrent gateway/CLI access; same idea as OpenClaw's
* session write lock, but scoped to a single transcript path with a smaller surface area.
*/
const DEFAULT_TIMEOUT_MS = 6e4;
const DEFAULT_STALE_MS = 1800 * 1e3;
const heldLocks = /* @__PURE__ */ new Map();
/** Same-process serialization for acquire; cross-process exclusion uses the lock file. */
const acquireChains = /* @__PURE__ */ new Map();
function chainPerTranscript(normalized, fn) {
	const run = (acquireChains.get(normalized) ?? Promise.resolve()).then(() => fn());
	acquireChains.set(normalized, run.then(() => void 0).catch(() => void 0));
	return run;
}
function normalizeTranscriptPath(transcriptAbsPath) {
	return path.resolve(transcriptAbsPath);
}
function lockPathFor(transcriptAbsPath) {
	const dir = path.dirname(transcriptAbsPath);
	const base = path.basename(transcriptAbsPath);
	const hash = createHash("sha256").update(transcriptAbsPath).digest("hex").slice(0, 16);
	return path.join(dir, `.${base}.${hash}.xopc-transcript.lock`);
}
function isPidAlive(pid) {
	if (!Number.isFinite(pid) || pid <= 0) return false;
	try {
		process.kill(pid, 0);
		return true;
	} catch {
		return false;
	}
}
async function readPayload(lockFile) {
	try {
		const raw = await readFile(lockFile, "utf8");
		return JSON.parse(raw);
	} catch {
		return null;
	}
}
async function isStaleLock(lockFile, staleMs) {
	let st;
	try {
		st = await stat(lockFile);
	} catch {
		return true;
	}
	if (Date.now() - st.mtimeMs > staleMs) return true;
	const payload = await readPayload(lockFile);
	const pid = typeof payload?.pid === "number" ? payload.pid : void 0;
	if (pid != null && pid !== process.pid && !isPidAlive(pid)) return true;
	return false;
}
async function releaseHeldLock(normalized, held) {
	held.refcount -= 1;
	if (held.refcount > 0) return;
	heldLocks.delete(normalized);
	try {
		await held.handle.close();
	} catch {}
	try {
		await rm(held.lockFile, { force: true });
	} catch {}
}
/**
* Acquire an exclusive lock for mutations to `transcriptAbsPath`.
* Re-entrant on the same path in the same process (nested `withTranscriptFileLock`).
*/
async function acquireTranscriptFileLock(transcriptAbsPath, opts) {
	const normalized = normalizeTranscriptPath(transcriptAbsPath);
	return chainPerTranscript(normalized, () => acquireTranscriptFileLockBody(normalized, opts));
}
async function acquireTranscriptFileLockBody(normalized, opts) {
	const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
	const staleMs = opts?.staleMs ?? DEFAULT_STALE_MS;
	const existing = heldLocks.get(normalized);
	if (existing) {
		existing.refcount += 1;
		return { release: async () => {
			await releaseHeldLock(normalized, existing);
		} };
	}
	const lockFile = lockPathFor(normalized);
	await mkdir(path.dirname(lockFile), { recursive: true });
	const started = Date.now();
	let handle = null;
	while (Date.now() - started < timeoutMs) try {
		handle = await open(lockFile, "wx", 384);
		break;
	} catch (err) {
		if (err.code === "EEXIST") {
			if (await isStaleLock(lockFile, staleMs)) {
				await rm(lockFile, { force: true });
				continue;
			}
			await new Promise((r) => setTimeout(r, 50 + Math.floor(Math.random() * 40)));
			continue;
		}
		throw err;
	}
	if (!handle) throw new Error(`timeout acquiring transcript lock: ${lockFile}`);
	const payload = {
		pid: process.pid,
		createdAt: (/* @__PURE__ */ new Date()).toISOString()
	};
	await handle.writeFile(JSON.stringify(payload, null, 2), "utf8");
	const held = {
		refcount: 1,
		handle,
		lockFile
	};
	heldLocks.set(normalized, held);
	return { release: async () => {
		await releaseHeldLock(normalized, held);
	} };
}
async function withTranscriptFileLock(transcriptAbsPath, fn, opts) {
	const lock = await acquireTranscriptFileLock(transcriptAbsPath, opts);
	try {
		return await fn();
	} finally {
		await lock.release();
	}
}
/** Best-effort: remove a stale lock without acquiring (e.g. doctor tooling). */
async function tryRemoveStaleTranscriptLock(transcriptAbsPath, staleMs = DEFAULT_STALE_MS) {
	const lockFile = lockPathFor(normalizeTranscriptPath(transcriptAbsPath));
	if (!existsSync(lockFile)) return false;
	if (!await isStaleLock(lockFile, staleMs)) return false;
	await rm(lockFile, { force: true });
	return true;
}
//#endregion
export { acquireTranscriptFileLock, tryRemoveStaleTranscriptLock, withTranscriptFileLock };

//# sourceMappingURL=transcript-file-lock.js.map