diff --git a/src/query.ts b/src/query.ts index 8bfca6111..2340c0c72 100644 --- a/src/query.ts +++ b/src/query.ts @@ -330,6 +330,11 @@ async function* queryLoop( // sites. let taskBudgetRemaining: number | undefined = undefined + // Guard against stop_hook_blocking infinite loops (see ~line 1326). + // Loop-local to avoid touching the 7 continue sites on State. + const MAX_STOP_HOOK_BLOCKING_RETRIES = 3 + let stopHookBlockingCount = 0 + // Snapshot immutable env/statsig/session state once at entry. See QueryConfig // for what's included and why feature() gates are intentionally excluded. const config = buildQueryConfig() @@ -1324,6 +1329,13 @@ async function* queryLoop( } if (stopHookResult.blockingErrors.length > 0) { + stopHookBlockingCount++ + if (stopHookBlockingCount > MAX_STOP_HOOK_BLOCKING_RETRIES) { + yield createAssistantAPIErrorMessage({ + content: `Stop hook blocked ${stopHookBlockingCount} times — stopping to prevent infinite loop.`, + }) + return { reason: 'completed' } + } const next: State = { messages: [ ...messagesForQuery, diff --git a/src/services/acp/agent.ts b/src/services/acp/agent.ts index 092adfa09..0b48cc19a 100644 --- a/src/services/acp/agent.ts +++ b/src/services/acp/agent.ts @@ -505,6 +505,7 @@ export class AcpAgent implements Agent { includePartialMessages: true, replayUserMessages: true, initialMessages: opts.initialMessages, + maxTurns: 200, } const queryEngine = new QueryEngine(engineConfig) diff --git a/src/services/acp/bridge.ts b/src/services/acp/bridge.ts index edf9102d3..450f64857 100644 --- a/src/services/acp/bridge.ts +++ b/src/services/acp/bridge.ts @@ -573,6 +573,7 @@ export async function forwardSessionUpdates( // Race the next message against the abort signal so we unblock // immediately when cancelled, even if the generator is waiting for // a slow API response. + let abortHandler: (() => void) | undefined const nextResult = await Promise.race([ sdkMessages.next(), new Promise>((resolve) => { @@ -580,10 +581,14 @@ export async function forwardSessionUpdates( resolve({ done: true, value: undefined }) return } - const handler = () => resolve({ done: true, value: undefined }) - abortSignal.addEventListener('abort', handler, { once: true }) + abortHandler = () => resolve({ done: true, value: undefined }) + abortSignal.addEventListener('abort', abortHandler, { once: true }) }), ]) + // Clean up: remove un-fired listener when generator completes first + if (abortHandler) { + abortSignal.removeEventListener('abort', abortHandler) + } if (nextResult.done || abortSignal.aborted) break const msg = nextResult.value