import { c as normalizeOptionalString, o as normalizeNullableString } from "./string-coerce-mnp54Vah.js";
import { b as isPlainObject } from "./utils-CCC-BEJH.js";
import { t as createSubsystemLogger } from "./subsystem-CHh-hY7v.js";
import { t as stableStringify } from "./stable-stringify-BL8fDhrH.js";
import { r as isMessagingToolSendAction } from "./embedded-agent-messaging-DTuPChZb.js";
import { createHash } from "node:crypto";
//#region src/agents/tool-loop-detection.ts
/**
* Tool-call loop detection.
*
* Watches recent tool history for repeated no-progress patterns and circuit-breaker thresholds.
*/
const log = createSubsystemLogger("agents/loop-detection");
const DEFAULT_LOOP_DETECTION_CONFIG = {
	enabled: false,
	historySize: 30,
	warningThreshold: 10,
	unknownToolThreshold: 10,
	criticalThreshold: 20,
	globalCircuitBreakerThreshold: 30,
	detectors: {
		genericRepeat: true,
		knownPollNoProgress: true,
		pingPong: true
	}
};
function selectHistoryForScope(history, scope) {
	const runId = normalizeOptionalString(scope?.runId);
	return history.filter((record) => normalizeOptionalString(record.runId) === runId);
}
function asPositiveInt(value, fallback) {
	if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return fallback;
	return value;
}
function resolveLoopDetectionConfig(config) {
	const warningThreshold = asPositiveInt(config?.warningThreshold, DEFAULT_LOOP_DETECTION_CONFIG.warningThreshold);
	let criticalThreshold = asPositiveInt(config?.criticalThreshold, DEFAULT_LOOP_DETECTION_CONFIG.criticalThreshold);
	let globalCircuitBreakerThreshold = asPositiveInt(config?.globalCircuitBreakerThreshold, DEFAULT_LOOP_DETECTION_CONFIG.globalCircuitBreakerThreshold);
	if (criticalThreshold <= warningThreshold) criticalThreshold = warningThreshold + 1;
	if (globalCircuitBreakerThreshold <= criticalThreshold) globalCircuitBreakerThreshold = criticalThreshold + 1;
	return {
		enabled: config?.enabled ?? DEFAULT_LOOP_DETECTION_CONFIG.enabled,
		historySize: asPositiveInt(config?.historySize, DEFAULT_LOOP_DETECTION_CONFIG.historySize),
		warningThreshold,
		unknownToolThreshold: asPositiveInt(config?.unknownToolThreshold, DEFAULT_LOOP_DETECTION_CONFIG.unknownToolThreshold),
		criticalThreshold,
		globalCircuitBreakerThreshold,
		detectors: {
			genericRepeat: config?.detectors?.genericRepeat ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.genericRepeat,
			knownPollNoProgress: config?.detectors?.knownPollNoProgress ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.knownPollNoProgress,
			pingPong: config?.detectors?.pingPong ?? DEFAULT_LOOP_DETECTION_CONFIG.detectors.pingPong
		}
	};
}
/**
* Hash a tool call for pattern matching.
* Uses tool name + deterministic JSON serialization digest of params.
*/
function hashToolCall(toolName, params) {
	return `${toolName}:${digestStable(params)}`;
}
function digestStable(value) {
	const serialized = stableStringify(value);
	return createHash("sha256").update(serialized).digest("hex");
}
function isKnownPollToolCall(toolName, params) {
	if (toolName === "command_status") return true;
	if (toolName !== "process" || !isPlainObject(params)) return false;
	const action = params.action;
	return action === "poll" || action === "log";
}
function extractTextContent(result) {
	if (!isPlainObject(result) || !Array.isArray(result.content)) return "";
	return result.content.filter((entry) => isPlainObject(entry) && typeof entry.type === "string" && typeof entry.text === "string").map((entry) => entry.text).join("\n").trim();
}
function formatErrorForHash(error) {
	if (error instanceof Error) return error.message || error.name;
	if (typeof error === "string") return error;
	if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return `${error}`;
	return stableStringify(error);
}
function extractUnknownToolName(error) {
	const raw = formatErrorForHash(error).trim();
	if (!raw) return;
	const toolName = (raw.match(/unknown tool[:\s]+["']?([a-z0-9_.-]+)["']?/i) ?? raw.match(/tool\s+["']?([a-z0-9_.-]+)["']?\s+(?:not found|is not available)/i))?.[1]?.trim();
	return toolName ? toolName.toLowerCase() : void 0;
}
function stringField(value) {
	return typeof value === "string" ? value : null;
}
function hashExecToolOutcome(details, text) {
	const status = stringField(details.status);
	if (!status) return;
	if (status === "running") return digestStable({
		status,
		tail: stringField(details.tail) ?? ""
	});
	if (status === "completed" || status === "failed") return digestStable({
		status,
		exitCode: typeof details.exitCode === "number" ? details.exitCode : null,
		timedOut: details.timedOut === true,
		output: normalizeNullableString(details.aggregated) ?? text
	});
	if (status === "approval-pending" || status === "approval-unavailable") return digestStable({
		status,
		reason: stringField(details.reason),
		host: stringField(details.host),
		command: stringField(details.command) ?? "",
		warningText: stringField(details.warningText) ?? ""
	});
}
const SEND_LIKE_MESSAGE_ACTIONS = new Set([
	"send",
	"broadcast",
	"reply",
	"thread-reply",
	"sendWithEffect",
	"sendAttachment",
	"upload-file",
	"sticker",
	"poll"
]);
const VOLATILE_SEND_RESULT_KEYS = new Set([
	"messageId",
	"message_id",
	"messageIds",
	"platformMessageId",
	"platformMessageIds",
	"fileId",
	"file_id",
	"fileKey",
	"pollId",
	"poll_id",
	"receipt",
	"runId",
	"idempotencyKey",
	"ts",
	"timestamp",
	"sentAt",
	"deliveredAt",
	"createdAt"
]);
function isMessageDeliveryObject(value) {
	return typeof value.id === "string" && typeof value.text === "string" && (typeof value.direction === "string" || typeof value.senderId === "string" || typeof value.accountId === "string" || isPlainObject(value.conversation));
}
function stripVolatileSendIds(value) {
	if (Array.isArray(value)) return value.map(stripVolatileSendIds);
	if (!isPlainObject(value)) return value;
	const dropMessageObjectId = isMessageDeliveryObject(value);
	const stripped = {};
	for (const [key, nested] of Object.entries(value)) {
		if (VOLATILE_SEND_RESULT_KEYS.has(key) || key === "id" && dropMessageObjectId) continue;
		stripped[key] = stripVolatileSendIds(nested);
	}
	return stripped;
}
function isVolatileSendResult(toolName, params) {
	if (toolName === "sessions_send") return true;
	const args = isPlainObject(params) ? params : {};
	if (toolName === "message") return typeof args.action === "string" && SEND_LIKE_MESSAGE_ACTIONS.has(args.action);
	return isMessagingToolSendAction(toolName, args);
}
function isLoopVetoResult(details) {
	return details.status === "blocked" && details.deniedReason === "tool-loop";
}
function hashToolOutcome(toolName, params, result, error) {
	if (error !== void 0) {
		const unknownToolName = extractUnknownToolName(error);
		return {
			resultHash: `error:${digestStable(formatErrorForHash(error))}`,
			unknownToolName
		};
	}
	if (!isPlainObject(result)) return { resultHash: result === void 0 ? void 0 : digestStable(result) };
	const details = isPlainObject(result.details) ? result.details : {};
	const text = extractTextContent(result);
	if (isLoopVetoResult(details)) return { resultHash: void 0 };
	if (toolName === "exec") {
		const execHash = hashExecToolOutcome(details, text);
		if (execHash) return { resultHash: execHash };
	}
	if (isKnownPollToolCall(toolName, params) && toolName === "process" && isPlainObject(params)) {
		const action = params.action;
		if (action === "poll") return { resultHash: digestStable({
			action,
			status: details.status,
			exitCode: details.exitCode ?? null,
			exitSignal: details.exitSignal ?? null,
			aggregated: details.aggregated ?? null,
			text
		}) };
		if (action === "log") return { resultHash: digestStable({
			action,
			status: details.status,
			totalLines: details.totalLines ?? null,
			totalChars: details.totalChars ?? null,
			truncated: details.truncated ?? null,
			exitCode: details.exitCode ?? null,
			exitSignal: details.exitSignal ?? null,
			text
		}) };
	}
	if (isVolatileSendResult(toolName, params)) return { resultHash: digestStable(stripVolatileSendIds(details)) };
	return { resultHash: digestStable({
		details,
		text
	}) };
}
function getUnknownToolRepeatStreak(history, toolName) {
	let streak = 0;
	let repeatedUnknownToolName;
	for (let i = history.length - 1; i >= 0; i -= 1) {
		const record = history[i];
		if (!record || record.toolName !== toolName || !record.unknownToolName) break;
		if (!repeatedUnknownToolName) {
			repeatedUnknownToolName = record.unknownToolName;
			streak = 1;
			continue;
		}
		if (record.unknownToolName !== repeatedUnknownToolName) break;
		streak += 1;
	}
	return {
		count: streak,
		unknownToolName: repeatedUnknownToolName
	};
}
function getNoProgressStreak(history, toolName, argsHash) {
	let streak = 0;
	let latestResultHash;
	for (let i = history.length - 1; i >= 0; i -= 1) {
		const record = history[i];
		if (!record || record.toolName !== toolName || record.argsHash !== argsHash) continue;
		if (typeof record.resultHash !== "string" || !record.resultHash) continue;
		if (!latestResultHash) {
			latestResultHash = record.resultHash;
			streak = 1;
			continue;
		}
		if (record.resultHash !== latestResultHash) break;
		streak += 1;
	}
	return {
		count: streak,
		latestResultHash
	};
}
function getPingPongStreak(history, currentSignature) {
	const last = history.at(-1);
	if (!last) return {
		count: 0,
		noProgressEvidence: false
	};
	let otherSignature;
	let otherToolName;
	for (let i = history.length - 2; i >= 0; i -= 1) {
		const call = history[i];
		if (!call) continue;
		if (call.argsHash !== last.argsHash) {
			otherSignature = call.argsHash;
			otherToolName = call.toolName;
			break;
		}
	}
	if (!otherSignature || !otherToolName) return {
		count: 0,
		noProgressEvidence: false
	};
	let alternatingTailCount = 0;
	for (let i = history.length - 1; i >= 0; i -= 1) {
		const call = history[i];
		if (!call) continue;
		const expected = alternatingTailCount % 2 === 0 ? last.argsHash : otherSignature;
		if (call.argsHash !== expected) break;
		alternatingTailCount += 1;
	}
	if (alternatingTailCount < 2) return {
		count: 0,
		noProgressEvidence: false
	};
	if (currentSignature !== otherSignature) return {
		count: 0,
		noProgressEvidence: false
	};
	const tailStart = Math.max(0, history.length - alternatingTailCount);
	let firstHashA;
	let firstHashB;
	let noProgressEvidence = true;
	for (let i = tailStart; i < history.length; i += 1) {
		const call = history[i];
		if (!call) continue;
		if (!call.resultHash) {
			noProgressEvidence = false;
			break;
		}
		if (call.argsHash === last.argsHash) {
			if (!firstHashA) firstHashA = call.resultHash;
			else if (firstHashA !== call.resultHash) {
				noProgressEvidence = false;
				break;
			}
			continue;
		}
		if (call.argsHash === otherSignature) {
			if (!firstHashB) firstHashB = call.resultHash;
			else if (firstHashB !== call.resultHash) {
				noProgressEvidence = false;
				break;
			}
			continue;
		}
		noProgressEvidence = false;
		break;
	}
	if (!firstHashA || !firstHashB) noProgressEvidence = false;
	return {
		count: alternatingTailCount + 1,
		pairedToolName: last.toolName,
		pairedSignature: last.argsHash,
		noProgressEvidence
	};
}
function canonicalPairKey(signatureA, signatureB) {
	return [signatureA, signatureB].toSorted().join("|");
}
/**
* Detect if an agent is stuck in a repetitive tool call loop.
* Checks if the same tool+params combination has been called excessively.
*/
function detectToolCallLoop(state, toolName, params, config, scope) {
	const resolvedConfig = resolveLoopDetectionConfig(config);
	if (!resolvedConfig.enabled) return { stuck: false };
	const history = selectHistoryForScope(state.toolCallHistory ?? [], scope);
	const currentHash = hashToolCall(toolName, params);
	const unknownToolStreak = getUnknownToolRepeatStreak(history, toolName);
	const noProgress = getNoProgressStreak(history, toolName, currentHash);
	const noProgressStreak = noProgress.count;
	const knownPollTool = isKnownPollToolCall(toolName, params);
	const pingPong = getPingPongStreak(history, currentHash);
	if (unknownToolStreak.count >= resolvedConfig.unknownToolThreshold) return {
		stuck: true,
		level: "critical",
		detector: "unknown_tool_repeat",
		count: unknownToolStreak.count,
		message: `CRITICAL: attempted unavailable tool ${unknownToolStreak.unknownToolName ?? toolName} ${unknownToolStreak.count} times. Stop retrying that missing tool and answer without it.`,
		warningKey: `unknown-tool:${toolName}:${unknownToolStreak.unknownToolName ?? "unknown"}`
	};
	if (noProgressStreak >= resolvedConfig.globalCircuitBreakerThreshold) {
		log.error(`Global circuit breaker triggered: ${toolName} repeated ${noProgressStreak} times with no progress`);
		return {
			stuck: true,
			level: "critical",
			detector: "global_circuit_breaker",
			count: noProgressStreak,
			message: `CRITICAL: ${toolName} has repeated identical no-progress outcomes ${noProgressStreak} times. Session execution blocked by global circuit breaker to prevent runaway loops.`,
			warningKey: `global:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
		};
	}
	if (knownPollTool && resolvedConfig.detectors.knownPollNoProgress && noProgressStreak >= resolvedConfig.criticalThreshold) {
		log.error(`Critical polling loop detected: ${toolName} repeated ${noProgressStreak} times`);
		return {
			stuck: true,
			level: "critical",
			detector: "known_poll_no_progress",
			count: noProgressStreak,
			message: `CRITICAL: Called ${toolName} with identical arguments and no progress ${noProgressStreak} times. This appears to be a stuck polling loop. Session execution blocked to prevent resource waste.`,
			warningKey: `poll:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
		};
	}
	if (knownPollTool && resolvedConfig.detectors.knownPollNoProgress && noProgressStreak >= resolvedConfig.warningThreshold) {
		log.warn(`Polling loop warning: ${toolName} repeated ${noProgressStreak} times`);
		return {
			stuck: true,
			level: "warning",
			detector: "known_poll_no_progress",
			count: noProgressStreak,
			message: `WARNING: You have called ${toolName} ${noProgressStreak} times with identical arguments and no progress. Stop polling and either (1) increase wait time between checks, or (2) report the task as failed if the process is stuck.`,
			warningKey: `poll:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
		};
	}
	const pingPongWarningKey = pingPong.pairedSignature ? `pingpong:${canonicalPairKey(currentHash, pingPong.pairedSignature)}` : `pingpong:${toolName}:${currentHash}`;
	if (resolvedConfig.detectors.pingPong && pingPong.count >= resolvedConfig.criticalThreshold && pingPong.noProgressEvidence) {
		log.error(`Critical ping-pong loop detected: alternating calls count=${pingPong.count} currentTool=${toolName}`);
		return {
			stuck: true,
			level: "critical",
			detector: "ping_pong",
			count: pingPong.count,
			message: `CRITICAL: You are alternating between repeated tool-call patterns (${pingPong.count} consecutive calls) with no progress. This appears to be a stuck ping-pong loop. Session execution blocked to prevent resource waste.`,
			pairedToolName: pingPong.pairedToolName,
			warningKey: pingPongWarningKey
		};
	}
	if (resolvedConfig.detectors.pingPong && pingPong.count >= resolvedConfig.warningThreshold) {
		log.warn(`Ping-pong loop warning: alternating calls count=${pingPong.count} currentTool=${toolName}`);
		return {
			stuck: true,
			level: "warning",
			detector: "ping_pong",
			count: pingPong.count,
			message: `WARNING: You are alternating between repeated tool-call patterns (${pingPong.count} consecutive calls). This looks like a ping-pong loop; stop retrying and report the task as failed.`,
			pairedToolName: pingPong.pairedToolName,
			warningKey: pingPongWarningKey
		};
	}
	const recentCount = history.filter((h) => h.toolName === toolName && h.argsHash === currentHash).length;
	if (!knownPollTool && resolvedConfig.detectors.genericRepeat && noProgressStreak >= resolvedConfig.criticalThreshold) {
		log.error(`Critical generic loop detected: ${toolName} repeated ${noProgressStreak} times`);
		return {
			stuck: true,
			level: "critical",
			detector: "generic_repeat",
			count: noProgressStreak,
			message: `CRITICAL: Called ${toolName} with identical arguments and identical outcomes ${noProgressStreak} times. Session execution blocked to prevent runaway loops.`,
			warningKey: `generic:${toolName}:${currentHash}:${noProgress.latestResultHash ?? "none"}`
		};
	}
	if (!knownPollTool && resolvedConfig.detectors.genericRepeat && recentCount >= resolvedConfig.warningThreshold) {
		log.warn(`Loop warning: ${toolName} called ${recentCount} times with identical arguments`);
		return {
			stuck: true,
			level: "warning",
			detector: "generic_repeat",
			count: recentCount,
			message: `WARNING: You have called ${toolName} ${recentCount} times with identical arguments. If this is not making progress, stop retrying and report the task as failed.`,
			warningKey: `generic:${toolName}:${currentHash}`
		};
	}
	return { stuck: false };
}
/**
* Record a tool call in the session's history for loop detection.
* Maintains sliding window of last N calls.
*/
function recordToolCall(state, toolName, params, toolCallId, config, scope) {
	const resolvedConfig = resolveLoopDetectionConfig(config);
	const runId = normalizeOptionalString(scope?.runId);
	if (!state.toolCallHistory) state.toolCallHistory = [];
	state.toolCallHistory.push({
		toolName,
		argsHash: hashToolCall(toolName, params),
		toolCallId,
		...runId && { runId },
		timestamp: Date.now()
	});
	if (state.toolCallHistory.length > resolvedConfig.historySize) state.toolCallHistory.splice(0, state.toolCallHistory.length - resolvedConfig.historySize);
}
/**
* Record a completed tool call outcome so loop detection can identify no-progress repeats.
*/
function recordToolCallOutcome(state, params) {
	const resolvedConfig = resolveLoopDetectionConfig(params.config);
	const runId = normalizeOptionalString(params.runId);
	const outcome = hashToolOutcome(params.toolName, params.toolParams, params.result, params.error);
	const resultHash = outcome.resultHash;
	if (!resultHash) return;
	if (!state.toolCallHistory) state.toolCallHistory = [];
	const argsHash = hashToolCall(params.toolName, params.toolParams);
	let matched = false;
	let recordedOutcome;
	for (let i = state.toolCallHistory.length - 1; i >= 0; i -= 1) {
		const call = state.toolCallHistory[i];
		if (!call) continue;
		if (normalizeOptionalString(call.runId) !== runId) continue;
		if (params.toolCallId && call.toolCallId !== params.toolCallId) continue;
		if (call.toolName !== params.toolName || call.argsHash !== argsHash) continue;
		if (call.resultHash !== void 0) continue;
		call.resultHash = resultHash;
		call.unknownToolName = outcome.unknownToolName;
		matched = true;
		recordedOutcome = call;
		break;
	}
	if (!matched) {
		const record = {
			toolName: params.toolName,
			argsHash,
			toolCallId: params.toolCallId,
			...runId && { runId },
			resultHash,
			unknownToolName: outcome.unknownToolName,
			timestamp: Date.now()
		};
		state.toolCallHistory.push(record);
		recordedOutcome = record;
	}
	if (state.toolCallHistory.length > resolvedConfig.historySize) state.toolCallHistory.splice(0, state.toolCallHistory.length - resolvedConfig.historySize);
	return recordedOutcome;
}
//#endregion
export { recordToolCall as n, recordToolCallOutcome as r, detectToolCallLoop as t };
