agent chased a circular pip upgrade for 6 hours: every resolve unpinned a different package and CI failed on a new...
Stops a circular pip upgrade loop where each resolver run unpins a different package and CI fails on a new package every cycle, by freezing the working pin set and upgrading one package at a time. Use when a dependency agent burns hours re-resolving without converging. Not for a single ResolutionImpossible error, wheel-build failures, or upgrades that resolve fine but break tests.
TL;DR
When every pip resolve unpins a different package, the agent is chasing a moving constraint set, not a solvable upgrade. Freeze the loop: capture the full working pin set, constrain the upgrade to one package at a time with pip-tools, and require the resolver to converge on the same tree twice before CI runs. Six hours of cycling means the upgrade path needs manual decomposition, not another blind retry.
The query
agent chased a circular pip upgrade for 6 hours: every resolve unpinned a different package and CI failed on a new one each timeUse this when
- A dependency agent re-resolves repeatedly, each run changing a different package.
- CI fails on a new package every cycle with no convergence.
- The agent has been retrying the same upgrade for hours.
Not for
- A single
ResolutionImpossibleerror (that is one conflict, not a loop). - Wheel-build failures during install.
- An upgrade that resolves cleanly but breaks tests (that is a code problem, not a resolver loop).
Steps
Step 1: Stop the loop and snapshot the last working tree
pip freeze
git stash list | head -3Expected output: the full list of installed versions. Save this output as working-pins.txt. It holds the exact versions that last passed CI, and it is the ground truth the agent keeps destroying by re-resolving. Everything from here is a controlled delta against this file.
Step 2: Read the resolver's actual conflict report
pip install --dry-run --report /tmp/resolve-report.json -r requirements.in
python -c "import json; d=json.load(open('/tmp/resolve-report.json')); print(d.get('conflicts', 'no conflict data'))"Expected output: the real constraint conflict, in the resolver's own words, instead of the agent's guess. The report names the packages whose requirements cannot all hold at once. That pair (or trio) is the circle.
Step 3: Upgrade exactly one package with pip-tools
pip-compile --upgrade-package "[package]" requirements.inExpected output: a new lockfile that changes only the target package and what it forces. If pip-compile reports it cannot resolve, the conflict from step 2 is genuine and needs a decision, not another retry.
Step 4: Run the dependent test subset before touching the next package
pip install -r requirements.txt
python -m pytest tests/ -k "[package]" -x -qExpected output: the tests covering the upgraded package pass. Only then move to the next package. One green package at a time is how the circle gets broken into a line.
Step 5: If two packages genuinely need incompatible versions, decide explicitly
grep -E "^[A-Za-z0-9_-]+==" working-pins.txt | grep -iE "[package-a]|[package-b]"Expected output: the pinned versions that work. The decision is: pin the loser at its working version, open a tracking issue for the incompatibility, and move on. An explicit pin is a resolution. Another resolve attempt is not.
Step 6: Give the agent a circuit breaker
Set max_resolve_attempts=3 in the agent config file (agent-config.ini).
Expected output: the config now caps the agent at 3 resolve attempts per package before it must escalate to a human with the conflict report from step 2. The 6-hour loop becomes a 15-minute escalation.
Variant phrasings
pip resolver backtracking forever on upgrade
Same moving-constraint problem. Steps 1-3 freeze the tree and upgrade one package at a time.
renovate circular bumps: A needs B at version 2 or above but B needs A below version 3
Same triangle in JS tooling. Group them and resolve the pair as one decision, not two alternating PRs.
every pip install --upgrade unpins something else
The requirements file is under-constrained. Steps 1 and 3 replace blind upgrading with pinned, single-package moves.
Why it happens
Pip's backtracking resolver explores combinations of constraints. When the upgrade spans mutually incompatible majors, each "fix" the agent applies invalidates the previous one: unpin X to satisfy Y, then Y's new version demands Z move, which re-breaks X. The agent sees each cycle as progress because something changed, but the constraint set is a circle and the resolver can walk it forever. The only exit is to stop resolving globally and upgrade locally, one package at a time, against a frozen pin set.
Edge cases
--upgrade-packagewithout a frozen baseline still wanders. Step 1's pin snapshot is what makes step 3 controlled.- Some circles are unresolvable by design (abandoned packages with hard pins). The circuit breaker in step 6 exists for exactly this: escalate instead of looping.
- CI caching a stale wheel can make a resolved tree fail for unrelated reasons. Clear the pip cache once before concluding the resolve is broken.
pip install --upgradeon an unpinned requirements file re-resolves everything. Never let the agent run a bare upgrade in the loop; always go through the compile step.- If the conflict involves a package with no newer compatible release, check for a fork or a shim before pinning forever. A pin is a pause, not a plan.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_luworuCTIqZjZqGErrGlag
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.