import type { Message, ToolCall } from '../../types/api.js'; // v1.11.6: doom-loop guard. When the model calls the same tool with the // same arguments DOOM_LOOP_THRESHOLD times in a row within one user-message // turn, abort the recursion and run the same wrap-up summary path as the // cap-hit case. Ported from opencode (DOOM_LOOP_THRESHOLD in // session/processor.ts). Threshold of 3 is the smallest value that doesn't // false-positive on a model that retries once after a transient error. export const DOOM_LOOP_THRESHOLD = 3; // Returns the name + args of the looping tool when the LAST // DOOM_LOOP_THRESHOLD entries in `recentToolCalls` are identical (same name // AND deep-equal args via JSON.stringify). Returns null otherwise. // Pure; exported for unit-test access. export function detectDoomLoop( recentToolCalls: ToolCall[], ): { name: string; args: Record } | null { if (recentToolCalls.length < DOOM_LOOP_THRESHOLD) return null; const last = recentToolCalls.slice(-DOOM_LOOP_THRESHOLD); const ref = last[0]!; const refArgs = JSON.stringify(ref.args); for (let i = 1; i < last.length; i++) { const tc = last[i]!; if (tc.name !== ref.name) return null; if (JSON.stringify(tc.args) !== refArgs) return null; } return { name: ref.name, args: ref.args }; } export function isCapHitSentinel(m: Message): boolean { return ( m.role === 'system' && m.metadata !== null && typeof m.metadata === 'object' && (m.metadata as { kind?: unknown }).kind === 'cap_hit' ); } // v1.11.6: parallel predicate. Same UI-only semantics as cap-hit sentinels — // never sent to the LLM (filtered by buildMessagesPayload through the // isAnySentinel check below). export function isDoomLoopSentinel(m: Message): boolean { return ( m.role === 'system' && m.metadata !== null && typeof m.metadata === 'object' && (m.metadata as { kind?: unknown }).kind === 'doom_loop' ); } export function isAnySentinel(m: Message): boolean { return isCapHitSentinel(m) || isDoomLoopSentinel(m); }