fix(background-agent): fall back to partInfo.input when state.input is unavailable for circuit breaker (fixes #3962)

The circuit breaker in manager.ts uses recordToolCall to detect when a subagent gets stuck repeating identical tool_use blocks. It passed partInfo.state?.input as the tool-input signature. When a model (Kimi K2.6 in the reporter's case) emits duplicate tool_use parts faster than the tool actually starts running, state.input is still null, so loop-detector falls back to the bare 'tool::__unknown-input__' signature. As soon as one part has state.input populated (next event), the signature flips to 'tool::{actual-args}' and the consecutive counter resets to 1, repeatedly. The breaker never reaches its 20-call threshold.

Add a top-level input?: Record<string, unknown> field to the local MessagePartInfo interface and prefer state.input when present, falling back to the part's own input when state is still pre-running. The OpenCode part payload carries the tool input as soon as the tool_use block is generated, so this fallback restores signature stability across the model's repeated emissions.

Verification: added 2 regression tests in manager-circuit-breaker.test.ts. Test 1 (reproduce) emits 20 part.updated events with only top-level input and asserts the task is cancelled by the breaker — fails before the fix, passes after. Test 2 confirms that when state.input IS present, it still wins over the top-level input (precedence preserved). All 10 manager-circuit-breaker tests pass, all 20 loop-detector tests pass, typecheck clean.
This commit is contained in:
MoerAI
2026-05-12 19:02:20 +09:00
parent 75825eb9a6
commit 5cd95cf57e
2 changed files with 103 additions and 1 deletions
+3 -1
View File
@@ -105,6 +105,7 @@ interface MessagePartInfo {
sessionID?: string
type?: string
tool?: string
input?: Record<string, unknown>
state?: { status?: string; input?: Record<string, unknown> }
}
@@ -1351,11 +1352,12 @@ The fallback retry session is now created and can be inspected directly.
const circuitBreaker = this.cachedCircuitBreakerSettings ?? resolveCircuitBreakerSettings(this.config)
this.cachedCircuitBreakerSettings = circuitBreaker
if (partInfo.tool) {
const toolInput = partInfo.state?.input ?? partInfo.input
task.progress.toolCallWindow = recordToolCall(
task.progress.toolCallWindow,
partInfo.tool,
circuitBreaker,
partInfo.state?.input
toolInput
)
if (circuitBreaker.enabled) {