/**
 * In-flight tool call tracking for autonomous mode idle detection.
 * Tracks which tool calls are currently executing so the idle watchdog
 * can distinguish "waiting for tool completion" from "truly idle".
 */
const inFlightTools = new Map();
/**
 * Tools that block waiting for human input by design.
 * The idle watchdog must not treat these as stalled.
 */
const INTERACTIVE_TOOLS = new Set(["ask_user_questions", "secure_env_collect"]);
/**
 * Mark a tool execution as in-flight.
 * Records start time and tool name so the idle watchdog can detect tools
 * hung longer than the idle timeout while exempting interactive tools.
 */
export function markToolStart(toolCallId, isActive, toolName) {
	if (!isActive) return;
	inFlightTools.set(toolCallId, {
		startedAt: Date.now(),
		toolName: toolName ?? "unknown",
	});
}
/**
 * Mark a tool execution as completed.
 */
export function markToolEnd(toolCallId) {
	inFlightTools.delete(toolCallId);
}
/**
 * Returns the age (ms) of the oldest currently in-flight tool, or 0 if none.
 */
export function getOldestInFlightToolAgeMs() {
	if (inFlightTools.size === 0) return 0;
	let oldestStart = Infinity;
	for (const t of inFlightTools.values()) {
		if (t.startedAt < oldestStart) oldestStart = t.startedAt;
	}
	return Date.now() - oldestStart;
}
/**
 * Returns the number of currently in-flight tools.
 */
export function getInFlightToolCount() {
	return inFlightTools.size;
}
/**
 * Returns the start timestamp of the oldest in-flight tool, or undefined if none.
 */
export function getOldestInFlightToolStart() {
	if (inFlightTools.size === 0) return undefined;
	let oldest = Infinity;
	for (const t of inFlightTools.values()) {
		if (t.startedAt < oldest) oldest = t.startedAt;
	}
	return oldest;
}
/**
 * Returns true if any currently in-flight tool is a user-interactive tool
 * (e.g. ask_user_questions, secure_env_collect) that blocks waiting for
 * human input. These must be exempt from idle stall detection.
 */
export function hasInteractiveToolInFlight() {
	for (const { toolName } of inFlightTools.values()) {
		if (INTERACTIVE_TOOLS.has(toolName)) return true;
	}
	return false;
}
/**
 * Clear all in-flight tool tracking state.
 */
export function clearInFlightTools() {
	inFlightTools.clear();
}
const MAX_TOP_TOOLS_IN_SUMMARY = 5;
const toolCallCountsByName = new Map();
export function resetToolCallCounts() {
	toolCallCountsByName.clear();
}
export function recordToolCallName(toolName) {
	if (!toolName) return;
	toolCallCountsByName.set(
		toolName,
		(toolCallCountsByName.get(toolName) ?? 0) + 1,
	);
}
export function formatToolCallSummary() {
	if (toolCallCountsByName.size === 0) return null;
	let total = 0;
	for (const count of toolCallCountsByName.values()) total += count;
	const ranked = [...toolCallCountsByName.entries()]
		.sort((a, b) => b[1] - a[1])
		.slice(0, MAX_TOP_TOOLS_IN_SUMMARY)
		.map(([name, count]) => `${name}×${count}`);
	return `${total} calls (top-${ranked.length}: ${ranked.join(", ")})`;
}
export function getTotalToolCallCount() {
	let total = 0;
	for (const count of toolCallCountsByName.values()) total += count;
	return total;
}
export function getToolCallCountSnapshot() {
	return Object.fromEntries(toolCallCountsByName.entries());
}
// ─── Tool invocation error classification (#2883) ────────────────────────
/**
 * Patterns that indicate a tool invocation failed deterministically before
 * useful work could be completed — as opposed to a normal business-logic error
 * from the tool handler. When these errors occur, retrying the same unit will
 * produce the same failure, so the retry loop must be broken.
 */
const TOOL_INVOCATION_ERROR_RE =
	/Validation failed for tool|Expected ',' or '\}'(?: after property value)?(?: in JSON)?|Unexpected end of JSON|Unexpected token.*in JSON/i;
const DETERMINISTIC_POLICY_ERROR_RE =
	/(?:^|\b)(?:HARD BLOCK:|Blocked: \/queue is a planning tool|Direct writes to \.sf\/STATE\.md and \.sf\/next\.db are blocked|This is a mechanical gate)/i;
/**
 * Known deterministic policy error substrings. Each entry is a stable string
 * that will appear in the tool error text content when the corresponding
 * policy gate fires. Retrying these errors will always produce the same outcome.
 *
 * Add new entries here as new deterministic gates are introduced. Do NOT use
 * regex — explicit substrings keep the list auditable.
 */
export const DETERMINISTIC_POLICY_ERROR_STRINGS = [
	// save_summary write-gate: CONTEXT artifact blocked pending depth verification (#4973).
	"context write blocked",
	"CONTEXT without depth verification",
	// Raw write tool gate (#4973): shouldBlockContextWrite emits this for direct
	// write tool calls to *-CONTEXT.md paths.
	"CONTEXT.md without depth verification",
];
/**
 * Returns true if the error message indicates a deterministic policy gate
 * blocked the tool call before execution. Retrying the same unit without
 * changing behavior will hit the same gate, so autonomous mode should write a
 * blocker placeholder instead of re-dispatching (#4973).
 */
export function isDeterministicPolicyError(errorMsg) {
	if (!errorMsg) return false;
	return (
		DETERMINISTIC_POLICY_ERROR_RE.test(errorMsg) ||
		DETERMINISTIC_POLICY_ERROR_STRINGS.some((s) => errorMsg.includes(s))
	);
}
/**
 * Returns true if the error message indicates a deterministic invocation or
 * policy failure (as opposed to a normal tool execution error).
 */
export function isToolInvocationError(errorMsg) {
	if (!errorMsg) return false;
	return (
		TOOL_INVOCATION_ERROR_RE.test(errorMsg) ||
		isDeterministicPolicyError(errorMsg)
	);
}
/**
 * Returns true if the error message indicates the tool was skipped because
 * a queued user message interrupted the turn (#3595).  Retrying will produce
 * the same skip, so the unit should be paused rather than retried.
 */
export function isQueuedUserMessageSkip(errorMsg) {
	if (!errorMsg) return false;
	return /^Skipped due to queued user message\.?$/i.test(errorMsg.trim());
}
