Skip to content

Fix rollback/remove of an agent record superseded by a hosted pin (#933) - #934

Open
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
mainfrom
agent/fix-superseded-agent-record-rollback
Open

Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
mainfrom
agent/fix-superseded-agent-record-rollback

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #933

Root cause

After an agent→hosted migration where the patch is superseded (same name@version, new UUID B), .socket/manifest.json still records agent patch A while the lockfile pins hosted patch B. vex already recognises this as a superseded record (vex_record_superseded). rollback and remove don't: their agent leg (rollback_patches_inner) runs the in-place restore of record A against files that, after a reinstall, hold B's bytes. The before/after hash check then fails with "File has been modified after patching", so rollback reports partial_failure and exits 1 on every run. remove aborts with rollback_failed before its hosted leg runs, so the hosted pin stays live.

Fix

  • rollback::superseded_by_hosted(manifest, hosted_pins) maps each manifest record to the hosted uuid when a live hosted pin wires the same package release (canonical base purl, with composer version equivalence) and no pin for that release carries the record's own uuid.
  • rollback_patches_inner takes that map. If a superseded record's in-place restore fails before writing anything, because the copy holds neither side of the record (a hash mismatch or a missing file), the copy is left to the hosted leg's lock restore and the next install. The run gets a rollback_record_superseded warning and the purl lands in the new RollbackOutcome::superseded, not as a failure. A copy that still holds A's patched bytes (no reinstall yet) is restored in place as before. A Gradle hash directory this record never patched, such as the superseding jar's own download (refused with gradle_rollback_hash_mismatch), is treated the same way, but only when none of the record's files there still hash to its afterHash (66ec386 and 1494f42, from Bugbot). That probe reads through the FIFO-safe read_regular_to_bytes (ec6ca85, from Bugbot). A swapped jar with no backup (jvm_jar_backup_missing, raised only when the jar holds this record's patched members), an unreadable file and a missing before-blob all still fail as before.
  • rollback's manifest cleanup drops superseded records. remove passes the same map, so it proceeds to its hosted leg and restores the lock.
  • CLI_CONTRACT.md documents the new warning code and the manifest-cleanup rule.

The hosted scan itself is unchanged. It could also drop or flag record A when it re-pins, but a scan without a reinstall would then lose the record of A's bytes still in node_modules. Fixing this at rollback/remove covers both orders.

Ported fix: 9c0a0c3 cherry-picks #878 ("Route Gradle digests through utils::digest"). main fails socket-patch-core --lib (production_digests_go_through_the_helpers), which turns test and coverage red on every PR. The commit becomes a no-op once #878 merges.

Tests (red → green)

New in crates/socket-patch-cli/tests/in_process_rollback_hosted.rs. Each runs a real in-process scan --mode hosted that pins superseding patch B over a manifest with agent record A:

  • rollback_drops_an_agent_record_superseded_by_a_hosted_pin: the tree holds B's bytes. Before the fix: exit 1, partial_failure, failed: 1, record kept. After: exit 0, rollback_record_superseded, lock byte-identical to pristine, record dropped, B's bytes untouched, and a re-run exits 0.
  • rollback_restores_a_superseded_agent_record_still_installed: the tree still holds A's bytes. This is a control and passed before and after: A is restored in place and there's no warning.
  • remove_unhosts_a_package_whose_agent_record_is_superseded: before the fix, exit 1 with rollback_failed and the pin kept live. After: exit 0, lock restored, record dropped, warning reported.

Unit tests in commands::rollback::tests: superseded_skip_covers_a_copy_holding_neither_side, superseded_skip_covers_a_gradle_dir_the_record_never_patched, superseded_skip_keeps_jvm_copies_holding_the_patched_bytes (red on 66ec386), superseded_skip_never_blocks_on_a_fifo, superseded_skip_leaves_other_failures_and_records_alone and superseded_by_hosted_needs_a_pin_with_another_uuid.

Local results:

  • cargo test -p socket-patch-cli --all-features for --test in_process_rollback_hosted (26), rollback (38), remove (91), in_process_remove_repair_lifecycle (23), in_process_rollback_all_ecosystems (8) and gradle_agent_cli (33): all pass. The --lib superseded unit tests (8) pass too.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo fmt: only the changed hunks were formatted, because main itself isn't cargo fmt --check clean and CI doesn't run fmt.
  • cargo test --workspace --all-features --no-fail-fast (on b43e50c+9c0a0c3): 226 test binaries green. The 11 failures are all write-failure or unremovable-file tests that rely on chmod denying writes, and they can't pass in this root sandbox: covgap_commands_vendor ×3, in_process_redirect ×3, repair ×2, and core copy_tree / vlt_heal / pypi_poetry / pypi_requirements. None of them touch rollback, remove or hosted pin code. CI runs as a non-root user.
  • No wrapper changes needed: the npm/, pypi/ and gem/ wrappers only dispatch to the binary.

Per-issue checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_01YbFRgXi519YoPqEJgyU5pS


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Regression tests for #933: an agent-mode record A left in the
manifest after a hosted scan pinned the superseding patch B. Rollback
and remove must restore the lock, drop record A and exit 0, both
after a reinstall (tree holds B's bytes) and before one (tree still
holds A's bytes).

Assisted-by: Claude Code:claude-opus-5-5
After an agent-to-hosted migration where the patch was replaced, the
manifest still recorded agent patch A while the lockfile pinned hosted
patch B. Rollback tried to restore A in place over B's installed bytes
and exited 1 with "modified after patching" on every run; remove
aborted the same way before restoring the lock, leaving B live.

A manifest record whose package release a live hosted pin wires to a
different patch is now superseded. When its installed copy holds
neither side of the record, rollback and remove leave the copy to the
hosted lock restore and the next install, report
rollback_record_superseded and drop the record instead of failing. A
copy that still holds A's patched bytes is restored as before.

Fixes #933

Assisted-by: Claude Code:claude-opus-5-5
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] coverage failed on b43e50c in socket-patch-core --lib, at utils::digest::tests::production_digests_go_through_the_helpers. That isn't this PR's failure: main has been red there since Gradle support (#646) and the digest helpers (#865) both landed, and open PR #878 fixes it. I ported #878's commit here as 9c0a0c3 (cherry-picked, behaviour unchanged). It becomes a no-op once #878 merges. Locally that test passes with the port.


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 6, 2026 13:08
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs
A Gradle or Maven copy holding the superseding hosted patch's jar
fails rollback before file verification: its hash directory is not
the download the old record patched (gradle_rollback_hash_mismatch),
or its swapped jar has no backup here (jvm_jar_backup_missing). Those
copies now get the same rollback_record_superseded handling as a
plain hash mismatch, so remove no longer aborts on them before
restoring the lock. Neither refusal writes anything.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs Outdated
jvm_jar_backup_missing is only raised once every jar member verifies
as the old record's patched bytes, so treating it as superseded would
drop the record while the patched jar stays in the cache. It now
fails as before.

A gradle_rollback_hash_mismatch copy is left to the hosted leg only
when none of the record's files there still hash to its patched
bytes; a corrupt before-blob for the directory the record did patch
fails as before.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs
The superseded-copy probe read installed files with a bare read, so a
FIFO or device planted in a Gradle cache directory would block
rollback and remove forever. It now uses read_regular_to_bytes like
the neighbouring Gradle checks; a non-regular file is refused and
counts as possibly patched, so the copy fails as before.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ec6ca85. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 6, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at head ec6ca85.

  • CI: 535/535 check runs green on ec6ca85 (6 skipped by matrix filters). The PR merges cleanly and is not behind main.
  • Bugbot: reviewed ec6ca85 and found no new issues. All 3 earlier Bugbot threads were fixed in follow-up commits and are resolved.
  • Reviewers: the earlier approval was given on 66ec386, so this head needs a re-approval. The core change is in crates/socket-patch-cli/src/commands/rollback.rs, which now skips the in-place agent restore for a manifest record that a live hosted pin superseded. Small cleanups touch gradle_cache.rs, jvm_jar.rs and sidecars/maven.rs.
  • Slack: not announced yet, because this run's Slack connector has no send tool. The next run will retry.

Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants