Repository navigation
After an agent→hosted migration, a superseding patch leaves the stale agent manifest record, so npm rollback exits 1 ("modified after patching") and remove refuses to un-host #933
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
A project that was patched in agent mode (for example by v4.0.0's default scan) keeps its .socket/manifest.json record when a v5 bare scan (hosted) takes the package over. That's fine as long as the API still offers the same patch. But if the patch has been superseded (same name@version, new UUID with different bytes) by the time the hosted scan runs:
- the hosted scan pins the new UUID B, reports
updates: [{oldUuid: A, newUuid: B}], and leaves manifest record A in place with no warning; npm ciinstalls B's bytes;vexsees the conflict and handles it (the recorded patch A is superseded by the lockfile-wired patch B; attesting only what the lockfile wires);rollbackrestores the lock to upstream but then fails the agent leg on record A:Cannot roll back: package/index.js - File has been modified after patching,partial_failure, exit 1. Re-runningrollbackkeeps exiting 1 until a reinstall happens to put the original bytes back.remove pkg:npm/left-pad@1.3.0fails the same way before the hosted leg runs: exit 1, and the lock still pins hosted patch B. The only way out isremove --skip-rollback(drops A only) followed by anotherremove, or a manualnpm cibetween rollbacks.listshows two patches for the one purl (agent A and hosted B).
Impact
This is the main v4 → v5 upgrade path (v4 defaulted to agent mode, v5 defaults to hosted), combined with an ordinary patch update. After that, rollback and remove (the documented ways to unpatch) fail with an error that blames the user ("modified after patching"), and remove leaves the hosted pin live. If the patch isn't superseded, the same migration rolls back cleanly (exit 0), because the hosted bytes equal A's after-bytes.
Repro (Linux, npm 10.9.4; mock patch API serving left-pad@1.3.0 as UUID A, later replaced by UUID B)
API="--api-url $U --org o --api-token fake --patch-server-url $U"
mkdir p && cd p && echo '{"name":"p","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json && npm install
# API offers A
socket-patch scan --mode agent --yes $API # or v4.0.0's bare `scan`; manifest records A
# API now offers only B (supersedes A)
socket-patch scan --yes $API # hosted: exit 0, updates[] A→B, lock pins B, manifest still A
rm -rf node_modules && npm ci # installs B's bytes
socket-patch rollback --yes $API # lock restored, then: "Failed to roll back: … File has been modified after patching" exit 1
socket-patch rollback --yes $API # exit 1 again
# on a copy taken before rollback:
socket-patch remove pkg:npm/left-pad@1.3.0 --yes $API # exit 1, lock still pins BExpected vs actual
- CLI_CONTRACT "Rollback command contract": rollback moves the project "toward fully unpatched" across the agent, vendored and hosted legs, and exit 1 is for things that leave "the system still patched". Here the only thing left is a manifest record that the hosted scan itself superseded.
vexalready treats it as superseded (vex_record_superseded,vex_sources.rs:359), but rollback and remove don't. - Expected: either the hosted scan that re-pins the purl to B drops or flags the superseded manifest record A (a warning such as
hosted_wiring_retained, which agent mode gives in the reverse direction), or rollback/remove treat a manifest record superseded by a live hosted pin as handled by the hosted leg.removeshould never exit with the hosted pin still live and only--skip-rollbackas a remedy. - Actual: exit 1 on every rollback until a reinstall, and remove refuses with the hosted pin kept.
Matrix (Linux; real npm installs, cold cache)
| npm | agent(A) → hosted(B), rollback | remove | agent(A) → hosted(A) control |
|---|---|---|---|
| 8.19.4 (Node 22) | exit 1 (lock byte-exact) | — | — |
| 10.9.4 (Node 22) | exit 1 ×2; exit 0 after npm ci |
exit 1, pin B kept | rollback exit 0 |
| 12.2.0 (Node 24) | exit 1 (lock byte-exact) | — | — |
| v4.0.0 agent manifest → main hosted (npm 10) | exit 1 | — | — |
Other superseding cells passed: hosted→hosted, vendored→vendored and agent→agent re-pin to B on npm 8/10/12 (plain and alias); npm ci installs B; vex attests B; rollback is byte-exact. Cross-mode hosted→vendored, vendored→hosted and agent→vendored with superseding also pass.
First bad
Not bisected: v5 hosted-by-default exists only on main (9c43dfc, which still reports 4.0.0). Reproduces on main 9c43dfc.
Suspect code
crates/socket-patch-cli/src/commands/rollback.rs:1436: the agent leg runs over every manifest record, including one superseded by a live hosted pin (the hosted leg ran first and succeeded).crates/socket-patch-core/src/patch/rollback.rs:172:HashMismatch"modified after patching" when the file holds the superseding patch's bytes.crates/socket-patch-cli/src/commands/remove.rs(~587): the agent rollback failure aborts before the hosted leg.- Hosted scan (
commands/scan): doesn't reconcile or warn about a manifest record for a purl it re-pins to a different UUID. Comparevex_sources.rs:359(vex_record_superseded).
No probe runs: probe branches are on hold for this routine until stale branches can be deleted.
Activity
mikolalysenko commentedon Oct 6, 2026
[agent] Triaged as
priority:p1(npm). This isn't a duplicate. The nearest open reports, #864 (cargo), #682 (Go), #266 (Maven) and #650 (Hatch), describe hosted→hosted superseding leftovers in other ecosystems. This one is the cross-mode case where an agent manifest record is left behind. No open or merged PR addresses it: onmain(9c43dfc),commands/rollback.rsandcommands/remove.rshave no superseded-record handling. Only VEX does (vex_record_superseded). It isn't clustered yet: the likely shared boundary is a helper that decides when a manifest record is superseded by a live hosted pin, used by rollback, remove and VEX. It's eligible for a future claim.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Claiming this issue (single-issue cluster; root cause: rollback and remove run the agent in-place restore over a manifest record that a live hosted pin has already superseded). Branch: agent/fix-superseded-agent-record-rollback. Claim-ID: 2026-10-06T12:21:28Z-c37dc9
Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Draft PR: #934
Generated by Claude Code
Test rollback/remove of a superseded agent record
Let rollback/remove unwind superseded records
Document and test remove/rollback of a superseded generation by uuid