fix(background-agent): fall back to partInfo.input when state.input is unavailable for circuit breaker (fixes #3962)#3972
Merged
code-yeongyu merged 1 commit intoMay 15, 2026
Conversation
…s unavailable for circuit breaker (fixes code-yeongyu#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.
Collaborator
Author
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
No issues found across 2 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Auto-approved: The change is minimal and type-safe, falling back to partInfo.input only when state.input is unavailable, and the two new tests confirm both the bug fix and that state.input still takes precedence when present, so there is no risk of regression.
This was referenced May 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The circuit breaker in
manager.tsfails to detect repeated tool_use loops (Kimi K2.6 reading the same file 20+ times) because it tracks onlypartInfo.state?.input, which isnulluntil the tool transitions torunning. The signature-based deduplicator then alternates betweentool::__unknown-input__andtool::{actual-input}on every event, repeatedly resettingconsecutiveCountto 1 and never reaching the threshold.Fall back to
partInfo.input(the part's top-level input, available as soon as the tool_use block is emitted) whenstate.inputis unavailable so the signature stays stable across the model's repeated emissions.Root Cause
src/features/background-agent/manager.tsline 1354-1359 (inside themessage.part.updated/message.part.deltahandler):```typescript
task.progress.toolCallWindow = recordToolCall(
task.progress.toolCallWindow,
partInfo.tool,
circuitBreaker,
partInfo.state?.input // <-- null until status="running"
)
```
The reporter traced the symptom: when Kimi K2.6 generates 20+ identical `tool_use` blocks, those parts arrive on `message.part.updated` with `state` still empty (no `status`, no `input`). `recordToolCall` receives `undefined`, falls into the "unknown input" branch in `loop-detector.ts:39-44`, and produces signature `read::unknown-input`. The next event for the same part finally has `state.input` populated, signature flips to `read::{filePath:...}`, `consecutiveCount` resets to 1, and the cycle repeats — so the `consecutiveThreshold: 20` ceiling is never reached.
Changes
Diff: +103 / -1 lines across 2 files.
Reproduction (before fix)
```text
src\features\background-agent\manager-circuit-breaker.test.ts:
expect(received).toBe(expected)
Expected: "cancelled"
Received: "running"
(fail) BackgroundManager circuit breaker > #given duplicate tool_use blocks arrive without state.input but with top-level input > #when 20 identical reads arrive #then circuit breaker still detects the loop
9 pass
1 fail
```
Without the fix, 20 identical `{ tool: "read", input: { filePath: "..." } }` events leave the task in `running` state — the breaker never fires.
Verification (after fix)
```text
bun test src/features/background-agent/manager-circuit-breaker.test.ts
10 pass
0 fail
21 expect() calls
Ran 10 tests across 1 file. [511.00ms]
bun test src/features/background-agent/loop-detector.test.ts
20 pass
0 fail
22 expect() calls
bun run typecheck
$ tsc --noEmit (clean)
```
The new "state.input takes precedence" test confirms the existing semantics are preserved: when both fields are populated, signatures still differentiate per-call.
Test
Fixes #3962
Need help on this PR? Tag
@codesmithwith what you need.Summary by cubic
Make the circuit breaker use top-level tool input when
state.inputisn’t available so repeatedtool_useloops are detected. Fixes #3962 by preventing infinite reads when models emit input before tools start running.state.input; fall back to part-levelinputto keep signatures stable across events.inputonMessagePartInfoand pass the resolved input torecordToolCall.state.inputtakes precedence.Written for commit 5cd95cf. Summary will update on new commits.