## TL;DR
Stop the agent from bumping packages one at a time. Solve the whole dependency graph in a single constraint pass, cap the agent at 3 resolve attempts, and treat a version flipping back to a value it already had as a loop to escalate, not a state to resolve again. Circular loops come from greedy sequential bumps; a joint solve fixes them in one shot.

## The query

```text
renovate agent got stuck in a loop: bumped lodash, which required bumping webpack, which required bumping lodash again
```

## Use this when

- The agent's version history shows the same package bouncing between two versions
- An upgrade PR keeps getting force-pushed with "fix constraints" commits
- CI fails on a different package each iteration of the loop
- The agent has burned hours of compute re-running the resolver

## Not for

- A peer-dependency conflict inside a single install
- Lockfile merge conflicts from rebasing
- Registry 429s or timeouts during verification
- A genuinely unresolvable dependency set (nothing satisfies all constraints)

## Steps

### 1. Log every bump the agent made

List the package, the from-version, and the to-version for each attempt, in order. You need this to see the oscillation.

Expected output: a short list like `lodash 4.17.20 -> 4.17.21 -> 4.17.20`, showing the same version repeating.

### 2. Write down the full constraint set on paper

For each package the agent touched, write the constraint its dependents actually declare: `webpack 5.x requires lodash 4.17.21 or later` and `plugin-x requires lodash below 4.17.21` or whatever the real declarations are. Read them from package.json files and lockfiles, not from the agent's summary.

Expected output: every constraint for the bumped packages listed with the package that declares it.

### 3. Solve all of it in one pass

Run the package manager's resolver once against the full set, e.g. `npm install --package-lock-only` after pinning the target versions, or hand the constraint set to the Renovate/Dependabot config and let it propose one PR. No sequential single-package bumps.

Expected output: one resolved lockfile, or one clear "no solution" error naming the conflicting constraints.

### 4. Cap attempts and detect oscillation

Give the agent a hard limit: 3 resolve attempts per upgrade task. Keep a set of versions already tried per package; if a bump targets a version already in the set, stop and escalate.

Expected output: the agent halts after 3 attempts with a summary instead of looping indefinitely.

### 5. Escalate with the conflict named

The escalation message must name the two constraints that cannot both be satisfied, e.g. "webpack needs lodash>=4.17.21, but plugin-x caps lodash<4.17.21. Human needed."

Expected output: a review request or issue naming the conflicting pair, not a 47th bump attempt.

## Variant phrasings

### renovate automerge flip-flopping two PRs

Same fix. The oscillation detector in step 4 works across PRs: record attempted version pairs per package pair, not just per package.

### dependabot PR loop react 19 vs design system

Same fix, with an extra rule: never downgrade a package below a version another package in the same PR set requires. Check the constraint set first (step 2).

### circular pip upgrade every resolve unpins a different package

Same fix. `pip install` with all targets in one command lets the resolver see the full picture instead of chasing one conflict at a time.

## Why it happens

The agent bumps greedily: upgrade A, run the resolver, see B complain, upgrade B, see A complain. Each step is locally reasonable and globally a loop. Nothing in the agent's config tracks which versions it already tried, so it can revisit the same state forever. A joint solve breaks the pattern because the resolver sees every constraint at once.

## Edge cases

- Genuinely unresolvable sets: if the joint solve says no solution exists, that is the answer. Do not let the agent "try one more combo."
- Calver packages: an agent that treats calver as semver will pick wrong targets and loop forever. Check the versioning scheme before bumping.
- Peer dependencies: npm's peer resolution can force oscillation even with a joint solve. Pin the peer pair explicitly in step 3.
- Time-boxed agents: if the agent has a wall-clock limit, the loop eats it silently. Add the attempt cap in step 4 regardless of time budget.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_cH7Uh_Ag5ZFEP0kzeezDeA
