fix(background-agent): add circuit breaker to prevent subagent infinite loops

Adds a configurable maxToolCalls limit (default: 200) that automatically
cancels background tasks when they exceed the threshold. This prevents
runaway subagent loops from burning unlimited tokens, as reported in #2571
where a Gemini subagent ran 809 consecutive tool calls over 3.5 hours
costing ~$350.

The circuit breaker triggers in the existing tool call tracking path
(message.part.updated/delta events) and cancels the task with a clear
error message explaining what happened. The limit is configurable via
background_task.maxToolCalls in oh-my-opencode.jsonc.

Fixes #2571
This commit is contained in:
MoerAI
2026-03-16 10:28:38 +09:00
committed by sspark-kisane
parent 65ccc9b854
commit 3055454ecc
2 changed files with 17 additions and 0 deletions
+2
View File
@@ -11,6 +11,8 @@ export const BackgroundTaskConfigSchema = z.object({
/** Timeout for tasks that never received any progress update, falling back to startedAt (default: 1800000 = 30 minutes, minimum: 60000 = 1 minute) */
messageStalenessTimeoutMs: z.number().min(60000).optional(),
syncPollTimeoutMs: z.number().min(60000).optional(),
/** Maximum tool calls per subagent task before circuit breaker triggers (default: 200, minimum: 10). Prevents runaway loops from burning unlimited tokens. */
maxToolCalls: z.number().int().min(10).optional(),
})
export type BackgroundTaskConfig = z.infer<typeof BackgroundTaskConfigSchema>
+15
View File
@@ -878,6 +878,21 @@ export class BackgroundManager {
if (partInfo?.type === "tool" || partInfo?.tool) {
task.progress.toolCalls += 1
task.progress.lastTool = partInfo.tool
const maxToolCalls = this.config?.maxToolCalls ?? 200
if (task.progress.toolCalls >= maxToolCalls) {
log("[background-agent] Circuit breaker: tool call limit reached", {
taskId: task.id,
toolCalls: task.progress.toolCalls,
maxToolCalls,
agent: task.agent,
sessionID,
})
void this.cancelTask(task.id, {
source: "circuit-breaker",
reason: `Subagent exceeded maximum tool call limit (${maxToolCalls}). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`,
})
}
}
}