Repository navigation
After uv remove of a vendored package, scan --prune, vendor --revert, remove and rollback all keep it as "drifted", so vendor --check stays red and its suggested scan --prune fix loops #1140
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
A user vendors six into a uv project and later drops the dependency with uv remove six. That is ordinary dependency churn. uv cleans up after itself: it drops six from dependencies and the [tool.uv.sources] six = { path = ".socket/vendor/…" } line, then relocks, and uv lock --check / uv sync --locked pass. After that, socket-patch can never reclaim the vendored entry:
vendor --checkexits 1: "dependency removed: no lockfile resolves pkg:pypi/six@1.16.0 any more … runsocket-patch scan --mode vendored --pruneto revert the vendored entry".scan --mode vendored --pruneexits 0success. With--jsonit lists six undergc.keptVendoredEntries; the human output says only "No patches available for installed packages." The wheel and the ledger entry stay.vendor --revertexits 0 and keeps the entry ("uv.lock fragment for Some("six") changed since vendoring; left untouched" plusvendor_lock_entry_removed). It advises "undo the drift (restore the vendored lock entries or re-vendor)", which is impossible because the dependency is gone.remove pkg:pypi/six@1.16.0androllbackexit 1 (partialFailure) with the same drift warnings.vendor --checkstays at exit 1 afterwards, so a CI gate on it stays red until someone hand-edits.socket/vendor/state.json.
The Bun lane has the same shape (#1132), and npm has a related human-mode GC gap (#1127). This is the uv code path (revert_uv).
Repro (Linux; uv 0.5.31 and 0.12.23)
mkdir app && cd app
printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "idna==3.7"]\n' > pyproject.toml
uv lock && uv sync
socket-patch scan --mode vendored --yes # vendors six (exit 0)
uv remove six # uv drops the dep + its [tool.uv.sources] entry; uv lock --check passes
socket-patch vendor --check; echo $? # 1: "dependency removed … run scan --mode vendored --prune"
socket-patch scan --mode vendored --prune --yes --json | jq .gc.keptVendoredEntries # ["pkg:pypi/six@1.16.0"], exit 0
socket-patch vendor --revert --yes # exit 0, "Kept 1 drifted package … undo the drift and re-run"
socket-patch remove pkg:pypi/six@1.16.0 --yes # exit 1, partialFailure
socket-patch vendor --check; echo $? # still 1; .socket/vendor/pypi/<uuid>/six-…whl and the ledger entry remainI served patch data from a local mock of the patch API (six@1.16.0).
Expected vs actual
- Expected: once uv has removed every reference to the vendored artifact (no
.socket/vendor/pypi/<uuid>left inuv.lockorpyproject.toml, and no package entry for it), the revert has nothing left to restore. It should count as converged, soscan --prune(the remedyvendor --checkprints) reverts the entry, deletes the wheel and turnsvendor --checkgreen.revert_uvalready treats a vanished[manifest] overridesfragment as converged when "no surviving reference to this entry's uuid dir" remains. The package and requires-dist records lack that arm. - Actual: every unwind drift-keeps, and the advice loops (check → prune → kept → check).
Matrix (Linux; real uv remove / uv lock)
| uv | socket-patch | check message | scan --prune |
vendor --revert |
remove |
rollback |
check after |
|---|---|---|---|---|---|---|---|
| 0.12.23 | main 3b4ac84 |
"dependency removed … run scan --prune" | exit 0, kept | exit 0, kept | exit 1, kept | exit 1, kept | 1 |
| 0.5.31 | main 3b4ac84 |
same | – | – | exit 1, kept | – | 1 |
| 0.12.23 | b96a785 (before #1050) |
"wiring missing … re-run vendor" | exit 1, partialFailure | exit 0, kept | – | – | 1 |
So the stuck state predates #1050. What #1050 changed is the remedy: vendor --check now names scan --prune, and scan --prune exits 0 while keeping the entry.
Suspect code
crates/socket-patch-core/src/vendor/pypi_uv.rs:793 (revert_uv):
- The
uv_lock_package/uv_lock_requires_distarm (line 876) counts only "the recorded pre-vendor original is present" as converged, so a packageuv removedeleted is drift. respell_original(line 836) refuses the requires-dist record because pyproject.toml no longer declares six.
Neither arm checks the condition the overrides arm uses: no surviving reference to .socket/vendor/pypi/<uuid> anywhere in the lock or pyproject. The GC then reports kept (commands/vendor.rs run_vendor_gc), and unwired_check_failure keeps sending users to it.
Activity
uv: run 32 ledger (#1138, #1140)
mikolalysenko commentedon Oct 8, 2026
[agent] Triaged: priority:p1 (uv). No duplicate or open PR. The suspect arms are in
revert_uv(crates/socket-patch-core/src/vendor/pypi_uv.rs). Theuv_lock_package/uv_lock_requires_distrecords count only "the original is present" as converged. They don't use the overrides arm's "no surviving reference to the uuid dir" test. Related, but a different code path: #1142 (Pipenv named category) and #1132 (Bun) have the same symptom.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Claiming this issue (with #1132, #1142; shared root cause: the bun.lockb, uv and Pipenv vendored revert backends map a lock entry the package manager removed to
vendor_lock_entry_driftedinstead of the sharedvendor_lock_entry_removedarm the npm-family text backends use, so every unwind keeps the entry). Branch: agent/fix-vendored-removed-dep-drift. Claim-ID: 2026-10-08T16:20:32Z-8b1b90Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Draft PR: #1147
Generated by Claude Code
Let uv revert a dependency uv remove dropped
Let uv revert a dependency uv remove dropped
Test uv remove then vendor --revert with real uv
mikolalysenko commentedon Oct 8, 2026
[agent] Two more shapes from the uv bug-hunt routine (ledger #310) for #1147's tests, on main
4657813with uv 0.12.23 on Linux. Both still drift-keep:s.pydeclaressix==1.16.0andattrs, and is vendored (s.py+s.py.lockwired). Thenuv remove --script s.py six(nosocketreference left in either file). Results:vendor --checkexits 1,scan --pruneexits 0 and keeps the entry,vendor --revertexits 0 withvendor_lock_entry_drifted,remove pkg:pypi/six@1.16.0exits 1 (vendor_lock_entry_drifted), androllbackexits 1partial_failure. The wheel stays.uv remove six && uv add six==1.17.0(uv.lock now pins PyPI six 1.17.0, nosocketreference). Same outcome: check exits 1, prune exits 0 and keeps the entry, revert exits 0 and keeps the wheel.(A bare
uv add six==1.17.0without the remove keeps the vendoredpathsource, and uv keeps installing the vendored 1.16.0. That's uv behaviour, not this bug.)Generated by Claude Code