// SF Tool Watchdog — runtime tool-output observer
//
// Observes tool-result events at runtime and emits a steer (info /
// warning) when patterns like a repeated bash failure or a too-large
// tool result indicate the agent is stuck.
import { repeatedToolFailureSignal } from "./supervision/loop-signals.js";

const seenKeys = new Set();
const bashFailures = new Map();
const MAX_RESULT_CHARS = 6000;
const REPEAT_FAILURE_WINDOW_MS = 10 * 60 * 1000;
// Maximum number of seen keys to retain before clearing; prevents unbounded
// accumulation across long autonomous mode sessions spanning many units.
const MAX_SEEN_KEYS = 200;
export function resetToolWatchdog() {
	seenKeys.clear();
	bashFailures.clear();
}
function normalizeCommand(command) {
	return String(command ?? "")
		.replace(/\s+/g, " ")
		.trim();
}
function contentText(event) {
	return event.content
		.map((part) => (part.type === "text" ? part.text : ""))
		.join("\n")
		.slice(0, MAX_RESULT_CHARS);
}
function once(steer) {
	if (seenKeys.has(steer.key)) return undefined;
	// Reset at unit boundaries approximated by size cap so that identical
	// tool-failure keys from new units are not silently suppressed forever.
	if (seenKeys.size >= MAX_SEEN_KEYS) seenKeys.clear();
	seenKeys.add(steer.key);
	return steer;
}
function buildSteer(title, body) {
	return [
		`Tool watchdog steering: ${title}`,
		"",
		body,
		"",
		"Treat this as a narrow correction. Do not restart the task; adapt the next step and continue.",
	].join("\n");
}
function maybeBunSteer(event) {
	if (event.toolName !== "Bash") return undefined;
	const command = normalizeCommand(event.input.command);
	if (!/\b(bun|bunx)\b/.test(command)) return undefined;
	return once({
		key: "bun-command",
		severity: "warning",
		content: buildSteer(
			"avoid Bun in this project",
			"The operator explicitly requested Node/npm verification. Replace Bun commands with npm/node equivalents before continuing.",
		),
	});
}
function maybeKnownFailureSteer(text) {
	if (
		text.includes("Cannot find module") &&
		text.includes("/extensions/sf/commands/dispatcher.js")
	) {
		return once({
			key: "sf-dispatcher-import",
			severity: "warning",
			content: buildSteer(
				"SF dispatcher import failed",
				"The installed SF command extension is trying to import commands/dispatcher.js and cannot resolve it. Check the extension sync/build path before retrying /auto.",
			),
		});
	}
	if (
		text.includes("failed to update rules file") &&
		text.includes(".codex/rules/default.rules") &&
		text.includes("Permission denied")
	) {
		return once({
			key: "exec-policy-permission",
			severity: "warning",
			content: buildSteer(
				"exec policy rules file is not writable",
				"The Codex rules file cannot be updated. Inspect whether ~/.codex/rules/default.rules is a read-only symlink, replace it with a local writable copy if needed, then retry the policy update.",
			),
		});
	}
	return undefined;
}
function maybeRepeatedFailureSteer(event, text) {
	if (event.toolName !== "Bash" || !event.isError) return undefined;
	const command = normalizeCommand(event.input.command);
	if (!command) return undefined;
	const now = Date.now();
	const existing = bashFailures.get(command);
	const count =
		existing && now - existing.lastAt <= REPEAT_FAILURE_WINDOW_MS
			? existing.count + 1
			: 1;
	bashFailures.set(command, { count, lastAt: now });
	if (count < 2) return undefined;
	const excerpt = text.slice(0, 1200);
	const loopSignal = repeatedToolFailureSignal({
		toolName: event.toolName,
		command,
		count,
		windowMs: REPEAT_FAILURE_WINDOW_MS,
		excerpt,
	});
	return once({
		key: `repeat-failure:${command}`,
		severity: "warning",
		loopSignal,
		content: buildSteer(
			"repeated failing command",
			`The same bash command has failed ${count} times in this autonomous mode unit:\n\n${command}\n\nStop retrying it as-is. Read the error, inspect the relevant files/config, and choose a different repair or verification path.\n\nLatest result excerpt:\n${excerpt}`,
		),
	});
}
export function observeToolResult(event) {
	const text = contentText(event);
	return (
		maybeBunSteer(event) ??
		maybeKnownFailureSteer(text) ??
		maybeRepeatedFailureSteer(event, text)
	);
}
