import { a as toolResultToText } from "./types-BiobHM1D.js";
import { i as styleReplacementForVia, n as resolveOldString } from "./edit-utils-EGosADZq.js";
import { resolve } from "node:path";
//#region src/tools/read-state.ts
const STATE = /* @__PURE__ */ new WeakMap();
/**
* Get or lazily create the per-session read-state map. Returns `undefined`
* when no session is provided.
*
* Most tool callers should prefer {@link resolveReadStateMap}, which
* additionally honors an explicit `ctx.readState` (the path
* `spawn`'s `shareReadState: true` uses to forward the parent's map
* into a sessionless child). Use this helper directly only when the
* Session-keyed map is exactly what you want and you don't need to
* accept an override.
*/
function getReadState(session) {
	if (!session) return void 0;
	let map = STATE.get(session);
	if (!map) {
		map = /* @__PURE__ */ new Map();
		STATE.set(session, map);
	}
	return map;
}
/**
* Resolve the active read-state map from a tool context. An explicit
* `ctx.readState` wins (used by `spawn`'s `shareReadState` opt-in to
* forward the parent's map into a sessionless child); otherwise the
* usual `Session`-keyed lookup applies.
*
* Tools should call this helper instead of `getReadState(ctx.session)`
* directly so the `shareReadState` plumbing is honored uniformly.
*/
function resolveReadStateMap(ctx) {
	return ctx.readState ?? getReadState(ctx.session);
}
/**
* Mark a read-state entry as elided from the on-wire history WITHOUT
* un-registering it.
*
* Used by the loop's `elideStaleReads` pass. The entry must survive —
* deleting it made the `requireReadBeforeEdit` gate reject the very next
* edit on a file the model had demonstrably read and just edited (the
* gate's failure mode then defeats the gate: models degrade to raw shell
* edits that bypass the guard entirely). The `elided` flag instead
* defeats only the dedup stub: the next identical re-read returns the
* full body again, and `rememberRead` clears the flag.
*
* No-op when the path has no entry in the resolved map(s).
*/
function markReadStateElided(ctx, cwd, path) {
	const key = readStateKey(cwd, path);
	for (const map of [ctx.readState, ctx.session ? STATE.get(ctx.session) : void 0]) {
		const entry = map?.get(key);
		if (entry && entry.elided !== true) map.set(key, {
			...entry,
			elided: true
		});
	}
}
/**
* Canonical read-state key for a `(cwd, path)` pair.
*
* `ctx.execution.readFile(handle, path)` resolves the model-provided
* path against `handle.cwd` before touching disk — so `src/App.tsx`,
* `./src/App.tsx`, and `/abs/cwd/src/App.tsx` all read the **same**
* bytes. The read-state map MUST mirror that resolution: without it,
* a model that reads `src/App.tsx` and then edits `./src/App.tsx`
* trips the `requireReadBeforeEdit` gate for a file it demonstrably
* already saw (the gate's "has not been read in this session" message
* fires on a stale key).
*
* `node:path`'s `resolve(cwd, path)` short-circuits when `path` is
* already absolute, so absolute and relative shapes converge on the
* same canonical form. We don't `realpath()` symlinks — the file IS
* the path the model addressed, not the realpath behind it; the host's
* execution context decides how to dereference.
*/
function readStateKey(cwd, path) {
	return resolve(cwd, path);
}
/**
* 64-bit content hash: two independent 32-bit FNV-1a-style lanes (different
* offset bases + multipliers) concatenated as 16 hex chars. Fast,
* non-cryptographic — but wide enough that the `requireReadBeforeEdit`
* drift gate, which silently corrupts a file on a false "unchanged"
* verdict, isn't betting on a 32-bit birthday bound (~1 in 4 billion per
* pair; uncomfortably reachable over many files × many sessions). Entries
* live only in per-session in-memory maps, so widening needs no
* stored-format migration. Still cheaper than allocating a Buffer and
* pulling in `crypto`.
*/
function hashContent(text) {
	let h1 = 2166136261;
	let h2 = 2128831031;
	for (let i = 0; i < text.length; i++) {
		const c = text.charCodeAt(i);
		h1 = Math.imul(h1 ^ c, 16777619) >>> 0;
		h2 = Math.imul(h2 ^ c, 2246822507) >>> 0;
	}
	return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0");
}
const READ_FILE_DEFAULT_OFFSET = 1;
const READ_FILE_DEFAULT_LIMIT = 2e3;
const READ_FILE_DEFAULT_MAX_BYTES = 262144;
const READ_FILE_FOOTER_RE = /\n\n…(?:read lines|truncated)/;
const READ_FILE_LINE_RE = /^\d{1,9}\t(.*)$/;
/**
* Async replay hydrator for durable hosts that can cheaply read the current
* full file through their execution context. Transcript-only reconstruction is
* still preferred for full reads; partial/truncated read_file entries fall back
* to `readFileForHash` and hash those full current bytes.
*/
async function hydrateReadStateFromSessionAsync(session, cwd, options = {}) {
	if (!session) return 0;
	const readState = getReadState(session);
	if (!readState) return 0;
	const normalized = normalizeHydrateReadStateOptions(options);
	const pending = /* @__PURE__ */ new Map();
	const known = /* @__PURE__ */ new Map();
	const preexistingKeys = new Set(readState.keys());
	let hydrated = 0;
	for (const turn of session.turns) for (const block of turn.content) {
		if (block.type === "tool_call") {
			if (block.name === normalized.readFileToolName || block.name === normalized.writeFileToolName || block.name === normalized.editToolName || block.name === normalized.multiEditToolName) pending.set(block.id, {
				name: block.name,
				input: block.input
			});
			continue;
		}
		if (block.type !== "tool_result") continue;
		const call = pending.get(block.callId);
		if (!call) continue;
		pending.delete(block.callId);
		if (block.isError === true) continue;
		const next = await stateFromToolResultAsync(call, toolResultToText(block.output), turn, cwd, normalized, known);
		if (!next) continue;
		if (!preexistingKeys.has(next.key)) {
			readState.set(next.key, next.file.entry);
			hydrated += 1;
		}
		known.set(next.key, next.file);
	}
	return hydrated;
}
function normalizeHydrateReadStateOptions(options) {
	return {
		readFileToolName: options.readFileToolName ?? "read_file",
		writeFileToolName: options.writeFileToolName ?? "write_file",
		editToolName: options.editToolName ?? "edit",
		multiEditToolName: options.multiEditToolName ?? "multi_edit",
		defaultLineNumbers: options.defaultLineNumbers ?? true,
		...options.readFileForHash ? { readFileForHash: options.readFileForHash } : {}
	};
}
function stateFromToolResult(call, output, resultTurn, cwd, options, known) {
	if (call.name === options.readFileToolName) return readFileStateFromResult(call.input, output, resultTurn, cwd, options.defaultLineNumbers);
	if (call.name === options.writeFileToolName) return writeFileStateFromResult(call.input, output, resultTurn, cwd);
	if (call.name === options.editToolName) return editStateFromResult(call.input, output, resultTurn, cwd, known);
	if (call.name === options.multiEditToolName) return multiEditStateFromResult(call.input, output, resultTurn, cwd, known);
}
async function stateFromToolResultAsync(call, output, resultTurn, cwd, options, known) {
	if (call.name === options.readFileToolName) return await readFileStateFromResultAsync(call.input, output, resultTurn, cwd, {
		defaultLineNumbers: options.defaultLineNumbers,
		readFileForHash: options.readFileForHash
	});
	return stateFromToolResult(call, output, resultTurn, cwd, options, known);
}
function readFileStateFromResult(input, output, resultTurn, cwd, defaultLineNumbers) {
	const path = typeof input.path === "string" ? input.path : void 0;
	if (!path || !isSuccessfulFullReadOutput(output)) return void 0;
	const lineNumbers = typeof input.lineNumbers === "boolean" ? input.lineNumbers : defaultLineNumbers;
	const content = lineNumbers ? stripReadFileLineNumbers(output) : output;
	if (content === void 0) return void 0;
	const entry = {
		contentHash: hashContent(content),
		offset: normalizeReadInteger(input.offset, READ_FILE_DEFAULT_OFFSET),
		limit: normalizeReadInteger(input.limit, READ_FILE_DEFAULT_LIMIT),
		maxBytes: normalizeReadInteger(input.maxBytes, READ_FILE_DEFAULT_MAX_BYTES),
		lineNumbers,
		mtimeMs: resultTurn.createdAt
	};
	return {
		key: readStateKey(cwd, path),
		file: {
			content,
			entry
		}
	};
}
async function readFileStateFromResultAsync(input, output, resultTurn, cwd, options) {
	const reconstructed = readFileStateFromResult(input, output, resultTurn, cwd, options.defaultLineNumbers);
	if (reconstructed) return reconstructed;
	const path = typeof input.path === "string" ? input.path : void 0;
	if (!path || !options.readFileForHash || !isSuccessfulReadOutput(output)) return void 0;
	let content;
	try {
		content = await options.readFileForHash(path);
	} catch {
		return;
	}
	if (typeof content !== "string") return void 0;
	const lineNumbers = typeof input.lineNumbers === "boolean" ? input.lineNumbers : options.defaultLineNumbers;
	const entry = {
		contentHash: hashContent(content),
		offset: normalizeReadInteger(input.offset, READ_FILE_DEFAULT_OFFSET),
		limit: normalizeReadInteger(input.limit, READ_FILE_DEFAULT_LIMIT),
		maxBytes: normalizeReadInteger(input.maxBytes, READ_FILE_DEFAULT_MAX_BYTES),
		lineNumbers,
		mtimeMs: resultTurn.createdAt
	};
	return {
		key: readStateKey(cwd, path),
		file: {
			content,
			entry
		}
	};
}
function writeFileStateFromResult(input, output, resultTurn, cwd) {
	const path = typeof input.path === "string" ? input.path : void 0;
	const content = typeof input.content === "string" ? input.content : void 0;
	if (!path || content === void 0) return void 0;
	if (!output.startsWith(`Created ${path} (`) && !output.startsWith(`Updated ${path} (`)) return void 0;
	return {
		key: readStateKey(cwd, path),
		file: {
			content,
			entry: {
				contentHash: hashContent(content),
				offset: 0,
				limit: Number.POSITIVE_INFINITY,
				maxBytes: Number.POSITIVE_INFINITY,
				mtimeMs: resultTurn.createdAt
			}
		}
	};
}
function editStateFromResult(input, output, resultTurn, cwd, known) {
	const path = typeof input.path === "string" ? input.path : void 0;
	const oldString = typeof input.old_string === "string" ? input.old_string : void 0;
	const newString = typeof input.new_string === "string" ? input.new_string : void 0;
	if (!path || oldString === void 0 || newString === void 0) return void 0;
	if (!output.startsWith(`Edited ${path}: replaced `)) return void 0;
	const key = readStateKey(cwd, path);
	const prior = known.get(key);
	if (!prior) return void 0;
	const nextContent = applyEdit(prior.content, oldString, newString, input.replace_all === true);
	if (nextContent === void 0) return void 0;
	return {
		key,
		file: {
			content: nextContent,
			entry: {
				...prior.entry,
				contentHash: hashContent(nextContent),
				mtimeMs: resultTurn.createdAt
			}
		}
	};
}
function multiEditStateFromResult(input, output, resultTurn, cwd, known) {
	const path = typeof input.path === "string" ? input.path : void 0;
	const edits = Array.isArray(input.edits) ? input.edits : void 0;
	if (!path || !edits) return void 0;
	if (!output.startsWith(`Edited ${path}: applied `)) return void 0;
	const key = readStateKey(cwd, path);
	const prior = known.get(key);
	if (!prior) return void 0;
	const applied = parseMultiEditAppliedSteps(output, edits.length);
	let current = prior.content;
	for (let i = 0; i < edits.length; i++) {
		if (!applied.has(i)) continue;
		const step = edits[i];
		if (typeof step.old_string !== "string" || typeof step.new_string !== "string") return void 0;
		const next = applyEdit(current, step.old_string, step.new_string, step.replace_all === true);
		if (next === void 0) return void 0;
		current = next;
	}
	return {
		key,
		file: {
			content: current,
			entry: {
				...prior.entry,
				contentHash: hashContent(current),
				mtimeMs: resultTurn.createdAt
			}
		}
	};
}
function applyEdit(content, oldString, newString, replaceAll) {
	const match = resolveOldString(content, oldString);
	if (!match) return void 0;
	if (match.occurrences > 1 && !replaceAll) return void 0;
	const styledReplacement = styleReplacementForVia(newString, match.via, match.actual);
	return replaceAll ? content.split(match.actual).join(styledReplacement) : content.replace(match.actual, styledReplacement);
}
function parseMultiEditAppliedSteps(output, editCount) {
	const annotationMatch = /<edit-outcomes>\n([\s\S]*?)\n<\/edit-outcomes>/.exec(output);
	if (!annotationMatch) return new Set(Array.from({ length: editCount }, (_, i) => i));
	const applied = /* @__PURE__ */ new Set();
	for (const line of annotationMatch[1].split("\n")) {
		const match = /^#(\d+) applied$/.exec(line.trim());
		if (!match) continue;
		const index = Number(match[1]) - 1;
		if (index >= 0 && index < editCount) applied.add(index);
	}
	return applied;
}
function isSuccessfulFullReadOutput(output) {
	if (!isSuccessfulReadOutput(output) || READ_FILE_FOOTER_RE.test(output)) return false;
	return true;
}
function isSuccessfulReadOutput(output) {
	if (output.length === 0) return true;
	if (output.startsWith("File not found:") || output.startsWith("[binary file:") || output.startsWith("Image:") || output.startsWith("Document:") || output.startsWith("[image too large to inline:") || output.startsWith("[document:") || output.startsWith("[document too large to attach:") || output.startsWith("Image read failed:") || output.startsWith("Document read failed:") || output.includes(" unchanged since the previous read in this session ")) return false;
	return true;
}
function stripReadFileLineNumbers(output) {
	const lines = output.split("\n");
	const stripped = [];
	for (const line of lines) {
		const match = READ_FILE_LINE_RE.exec(line);
		if (!match) return void 0;
		stripped.push(match[1]);
	}
	return stripped.join("\n");
}
function normalizeReadInteger(value, fallback) {
	if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return fallback;
	return Math.floor(value);
}
const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
/**
* Get or lazily create the per-session tool-dedup map. Returns `undefined`
* when no session is provided — middleware should treat that as "no dedup".
*/
function getToolDedupState(session) {
	if (!session) return void 0;
	let map = TOOL_DEDUP_STATE.get(session);
	if (!map) {
		map = /* @__PURE__ */ new Map();
		TOOL_DEDUP_STATE.set(session, map);
	}
	return map;
}
//#endregion
export { markReadStateElided as a, hydrateReadStateFromSessionAsync as i, getToolDedupState as n, readStateKey as o, hashContent as r, resolveReadStateMap as s, getReadState as t };

//# sourceMappingURL=read-state-BFqpQRc5.js.map