Skip to content

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 into
code-yeongyu:devfrom
MoerAI:fix/circuit-breaker-tool-input-fallback
May 15, 2026
Merged

fix(background-agent): fall back to partInfo.input when state.input is unavailable for circuit breaker (fixes #3962)#3972
code-yeongyu merged 1 commit into
code-yeongyu:devfrom
MoerAI:fix/circuit-breaker-tool-input-fallback

Conversation

@MoerAI

@MoerAI MoerAI commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

The circuit breaker in manager.ts fails to detect repeated tool_use loops (Kimi K2.6 reading the same file 20+ times) because it tracks only partInfo.state?.input, which is null until the tool transitions to running. The signature-based deduplicator then alternates between tool::__unknown-input__ and tool::{actual-input} on every event, repeatedly resetting consecutiveCount to 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) when state.input is unavailable so the signature stays stable across the model's repeated emissions.

Root Cause

src/features/background-agent/manager.ts line 1354-1359 (inside the message.part.updated / message.part.delta handler):

```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

File Change
`src/features/background-agent/manager.ts` Add `input?: Record<string, unknown>` to the local `MessagePartInfo` interface so the part's top-level `input` is typed. In the tool-call handler, prefer `partInfo.state?.input` then fall back to `partInfo.input`.
`src/features/background-agent/manager-circuit-breaker.test.ts` Add 2 regression tests: (1) emit 20 part.updated events with only top-level input → assert the task is cancelled; (2) when both `state.input` and top-level `input` are present, `state.input` still takes precedence.

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

  • Regression tests: 2 new cases in `manager-circuit-breaker.test.ts` (one reproduces the bug, one guards the precedence semantics).
  • Related suite: `bun test src/features/background-agent/manager-circuit-breaker.test.ts loop-detector.test.ts` — 30 pass, 0 fail.
  • Typecheck: `bun run typecheck` — clean.

Fixes #3962


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by cubic

Make the circuit breaker use top-level tool input when state.input isn’t available so repeated tool_use loops are detected. Fixes #3962 by preventing infinite reads when models emit input before tools start running.

  • Bug Fixes
    • Prefer state.input; fall back to part-level input to keep signatures stable across events.
    • Typed input on MessagePartInfo and pass the resolved input to recordToolCall.
    • Added regression tests to cover top-level-only input and confirm state.input takes precedence.

Written for commit 5cd95cf. Summary will update on new commits.

…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.
@MoerAI

MoerAI commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@code-yeongyu code-yeongyu merged commit 83ab000 into code-yeongyu:dev May 15, 2026
8 checks passed
@MoerAI MoerAI deleted the fix/circuit-breaker-tool-input-fallback branch May 18, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Circuit breaker fails to detect repetitive tool calls when model generates duplicate tool_use blocks (Kimi K2.6)

2 participants