Repository navigation
uv hosted → vendored takeover onto a newer patch uuid fails "no installed package found on disk" (exit 1) when the venv holds the hosted patch's bytes, while --dry-run previews success #1105
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
In a uv project that hosted mode patched with patch A, and whose .venv was then synced from the hosted lock (so six.py holds A's patched bytes), scan --mode vendored / get --mode vendored fails once the API serves a superseding patch B for the same six@1.16.0:
skipped package_not_installed no installed package found on disk
Error: Cannot vendor pkg:pypi/six@1.16.0: no installed package found on disk
Vendored 0 packages; 1 not installed.
The run exits 1, and the hosted takeover never happens. --dry-run of the same command reports would_vendor / success with exit 0.
Six is installed. The installed bytes are the hosted patch A's, so they match neither of B's beforeHash / afterHash. From the code, the variant probe then drops the candidate, because superseded_install only recognises an older patch generation through the vendor ledger. A hosted pin at another uuid isn't considered, so the candidate falls through to the "not installed" bucket.
Impact
A user who has been on hosted mode can't move to vendored once a newer patch exists, unless they first delete or reinstall their venv from PyPI. The run exits 1 with a false diagnosis ("not installed"), and the dry run promises success. The project stays hosted on the older patch A, so nothing is lost; the migration and the upgrade are just blocked. It hits every common uv flow, because uv sync after a hosted scan always installs the hosted bytes.
Repro (Linux, uv 0.12.23)
Patch data came from a local mock patch server (the one used on this ledger's probe branches), with a single origin. Its batch query returns patch A (…0001, adds SOCKET_PATCHED = 1) until a flag file exists, and patch B (…0004, SOCKET_PATCHED = 2) afterwards. Both are for pkg:pypi/six@1.16.0 with the same beforeHash.
git init -q app && cd app
cat > pyproject.toml <<'P'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "attrs"]
P
uv lock --python 3.11 && uv sync --python 3.11
socket-patch scan --mode hosted --yes --json # exit 0 (patch A)
rm -rf .venv && uv sync --locked --python 3.11 # .venv now holds patch A's six.py
git add -A && git commit -qm hosted
# --- the API now serves superseding patch B for six@1.16.0 ---
socket-patch scan --mode vendored --dry-run --yes --json # exit 0, status success, six: would_vendor
socket-patch scan --mode vendored --yes --json # exit 1, partial_failure, package_not_installed
git status --short # nothing changed; still hosted on A
socket-patch get pkg:pypi/six@1.16.0 --mode vendored --yes --json # exit 1, same event
rm -rf .venv && socket-patch scan --mode vendored --yes --json # exit 0: takeover + vendored B (control)scan --json does list the upgrade (updates: [{oldUuid: …0001, newUuid: …0004}]), and then vendors nothing.
Expected vs actual
- Expected: CLI_CONTRACT.md "Takeover reconciliation":
scan/get --mode vendoredover a live hosted pin restores the upstream entry and vendors the selected patch. The vendored A → vendored B upgrade of the same package works (vendor_stale_artifact_removed), and so do vendored A → hosted B and hosted A → hosted B. Hosted A → vendored B should behave the same way, or at least refuse with an accurate code that the dry run predicts too (exit-code parity). - Actual: exit 1 with
package_not_installed("no installed package found on disk") although six is installed, and the dry run sayswould_vendor/ exit 0.
OS × version
| Cell | uv 0.5.31 | uv 0.8.17 | uv 0.12.23 |
|---|---|---|---|
uv project, .venv synced from hosted A, scan --mode vendored (B) |
fail | fail | fail (×3) |
same, get pkg:pypi/six@1.16.0 --mode vendored |
— | — | fail |
uv pip requirements.txt lane, venv reinstalled from the hosted pin |
— | — | fail |
control: lock-only checkout (no .venv) |
— | — | pass |
| control: hosted A → vendored A (same uuid) | — | — | pass |
| control: vendored A → vendored B, vendored A → hosted B, hosted A → hosted B (project, PEP 723 script lock, pylock.toml) | — | — | pass |
| PEP 723 script lock / pylock.toml, hosted A → vendored B (no project venv holding A) | — | — | pass |
All runs were on Linux. The cause is in candidate selection, with no OS-specific file handling, so I didn't run a macOS / Windows probe. Other PyPI lanes with an installed hosted copy (Poetry, PDM, Pipenv) may behave the same way. I only confirmed the uv project and the uv pip requirements lanes.
Bisect
Not bisected. The hosted → vendored takeover is v5 (main) behaviour; I tested main 77f305d.
Suspect code
crates/socket-patch-cli/src/commands/vendor.rs:2864: the variant probe drops the candidate unlesssuperseded_installsays the install is an older generation.crates/socket-patch-cli/src/commands/vendor.rs:2345:superseded_installonly consults the vendor ledger (entry.uuid != record.uuid). A live hosted pin at another uuid (hosted_pins, discovered atvendor.rs:2732) isn't treated as a superseded install, so the takeover atvendor.rs:2928is never reached.plan_service_downloads(vendor.rs:2413) uses the same check.- The dry-run preview doesn't predict this outcome.
No probe runs (Linux only).
Backlog review — 2026-10-08
Priority: P1 → P2. Upgrading a uv takeover fails while the old hosted patch remains present. A conditional upgrade failure, not patch loss.
Activity
mikolalysenko commentedon Oct 8, 2026
[agent] Triaged as priority:p1 (uv / PyPI family). Confirmed the suspect path on
mainb762f41:superseded_install(crates/socket-patch-cli/src/commands/vendor.rs:2345) only checks the vendor ledger for an older uuid, so an installed copy carrying a live hosted pin at another uuid is not treated as a superseded install and the candidate falls through topackage_not_installed. Not a duplicate of #944 (that one restores before the uv refusals run; here the takeover is never reached). No open PR covers it yet.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Poetry bug-hunt routine (ledger #311): this reproduces in the Poetry lane too, on main
3b4ac84(Linux, real Poetry, local mock patch API serving patch A then a superseding patch B forsix@1.16.0).Steps:
poetry lock(in-project venv) →scan --mode hosted(pins A) →poetry install --no-root(the venv holds A's bytes) → the API switches to B →scan --mode vendored.would_vendor, exit 0package_not_installed("no installed package found on disk")would_vendor, exit 0would_vendor, exit 0would_vendorwould_vendorSo the generic
superseded_installpath (commands/vendor.rs) hits Poetry the same way. Nothing is lost (the lock stays hosted on A), but the upgrade is blocked behind a false "not installed" with exit 1. A related Poetry-only gap, vendored A → vendored B, is filed separately as #1136.Generated by Claude Code
poetry: run 16 ledger (#1136, #1105 comment, pdm handover)