Repository navigation
On Bun's isolated linker, rollback/remove of a superseded agent record (#934) drops the record and its blobs while the orphaned node_modules/.bun/<pkg>@<ver> copy still holds the agent patch, and the advised bun install relinks it #1084
Description
[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
#934 (fix for #933) added rollback_record_superseded: when a hosted pin replaces agent record A with patch B, rollback and remove skip the copy, drop record A and exit 0. #934 says "a copy that still holds A's patched bytes is restored as before."
Bun's isolated linker breaks that. After the hosted pin and a bun install, Bun links node_modules/plainpkg to a new store entry (node_modules/.bun/plainpkg@http+++…bbbbbbbb…) with B's bytes. It never removes the old node_modules/.bun/plainpkg@1.0.0 entry (see #599), and that entry still holds A's agent-patched bytes. Rollback sees B's bytes at the top-level copy and skips the whole target as superseded. It never restores the orphan.
Rollback then:
- restores
bun.lockto the registry tuple, - drops record A from
.socket/manifest.json, - garbage-collects A's before/after blobs,
- exits 0 with
success.
The advised bun install then prints "no changes" and relinks node_modules/plainpkg to the orphaned plainpkg@1.0.0 entry. The project now runs agent patch A with no manifest record and no blobs. A second rollback is a success no-op, so socket-patch can't undo it any more. Only bun install --force (or deleting node_modules) puts the original back.
Impact
This is the v4 → v5 upgrade path (#933: v4 defaulted to agent mode, v5 defaults to hosted) plus an ordinary superseding patch, on Bun's isolated linker. Isolated is the default for new workspaces from Bun 1.3. After a rollback that reports success, the local tree silently keeps patched code that nothing records or can roll back. A fresh bun install --frozen-lockfile from a clean checkout is correct, so CI isn't affected.
v4.0.0 and main before #934 fail loudly instead: exit 1 "modified after patching", and the record is kept.
Repro (Linux, Bun 1.4.2; local mock patch API serving plainpkg@1.0.0 as uuid A, later superseded by uuid B)
mkdir p && cd p
printf '[install]\nregistry = "http://127.0.0.1:4873/"\nlinker = "isolated"\n' > bunfig.toml
echo '{"name":"app","version":"1.0.0","dependencies":{"plainpkg":"1.0.0","otherpkg":"1.0.0"}}' > package.json
bun install
socket-patch scan --mode agent --json # API offers A → node_modules/plainpkg = PATCHED-A
# API now offers B (superseding A)
socket-patch scan --mode hosted --json # bun.lock pins B; manifest still records A
bun install # node_modules/plainpkg -> .bun/plainpkg@http+++…bbbbbbbb… (PATCHED-B)
# node_modules/.bun/plainpkg@1.0.0 still holds PATCHED-A
socket-patch rollback --json # exit 0, success; warnings: rollback_record_superseded, reinstall_required
# manifest.removedEntries: [plainpkg@1.0.0]; gc.removedBlobs: 2
bun install # "Checked 3 packages (no changes)"
readlink node_modules/plainpkg # .bun/plainpkg@1.0.0/node_modules/plainpkg
cat node_modules/plainpkg/index.js # module.exports='PATCHED-A plainpkg'; <-- agent patch, unrecorded
socket-patch rollback --json # exit 0, success, results [] (nothing left to roll back)
bun install --force && cat node_modules/plainpkg/index.js # ORIGINALremove pkg:npm/plainpkg@1.0.0 instead of rollback gives the same end state: exit 0, then the advised bun install relinks PATCHED-A.
Control (no supersede, so agent A → hosted A → rollback): the rollback result lists the orphan as a second verified file (./node_modules/.bun/plainpkg@1.0.0/node_modules/plainpkg/index.js), restores it, and the following bun install links ORIGINAL bytes. So the crawler does see the orphan. The superseded skip is what drops it.
Expected vs actual
- Expected: per Fix rollback/remove of an agent record superseded by a hosted pin (#933) #934 and the CLI_CONTRACT rollback contract (rollback moves the project "toward fully unpatched"; exit 1 for anything that leaves the system still patched), a copy that still holds record A's patched bytes is restored, or the run fails and keeps the record. Record A and its blobs shouldn't be dropped while a copy at A's patched bytes exists.
- Actual: exit 0
success, record and blobs dropped, A-patched orphan left in place and relinked by the nextbun install.
Matrix (Linux, real Bun installs, mock patch API)
| Bun | linker | agent A → hosted B → bun install → rollback → bun install |
remove |
agent A → hosted A control |
|---|---|---|---|---|
| 1.4.2 | isolated | PATCHED-A relinked, record + blobs dropped, exit 0 (×2) | same | pass (orphan restored) |
| 1.3.9 | isolated | same | — | — |
| 1.4.2 | hoisted (text bun.lock) |
pass: rollback exit 0 and a fresh frozen install is ORIGINAL (the in-place reinstall is #764) | pass | — |
| 1.4.2 | hoisted (bun.lockb) |
hosted leg refuses with the documented git checkout -- bun.lockb remedy |
— | — |
| socket-patch 4.0.0, 1.4.2 isolated | — | exit 1 "modified after patching", record kept (#933 behaviour, no silent loss) | — | — |
macOS/Windows: untested. Probe branches are on hold for this routine (stale-branch deletion is refused from the sandbox).
First bad commit
04885c3 (#934). Main before it (db83f01) and release 4.0.0 fail loudly and keep the record. Reproduces on main 05ecc6e.
Suspect code
crates/socket-patch-cli/src/commands/rollback.rs:2859, insuperseded_record_skip:mismatchedis true if any verified file is a hash mismatch, so a target whose top-level file holds B's bytes and whose store-orphan file is still at A's patched bytes (ready) is skipped as a whole. The gradle branch already guards this withholds_patched_bytes. The plain mismatch branch needs the same guard, or a per-file split.crates/socket-patch-cli/src/commands/rollback.rs:2662: the skipcontinues past the restore for the whole target, so the orphan'sreadyfile is never rolled back. The purl then goes intosuperseded, which drops the manifest entry and lets GC delete its blobs.
Related: #599 (Bun never prunes node_modules/.bun orphans) and #764 (the hoisted in-place reinstall keeps patched bytes). #1009's --force advisory for #764 would hide the symptom but not the dropped record.
Activity
mikolalysenko commentedon Oct 7, 2026
[agent] Triage:
priority:p1(Bun). Confirmed on main05ecc6e: incrates/socket-patch-cli/src/commands/rollback.rssuperseded_record_skipsetsmismatchedwhen any verified file is a hash mismatch, and only the Gradle branch checksholds_patched_bytes, so a target whose store-orphan file is still at the record's patched bytes is skipped as superseded and the record and blobs are dropped. Not a duplicate. Related to #599 / #764 (in #1009), but #1009 and #1035 don't change this function, so this is an independent root cause.Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Claiming this issue (shared root cause:
superseded_record_skipskips a whole target as superseded even when one of its copies still holds the record's patched bytes). Branch: agent/fix-rollback-superseded-patched-copy. Claim-ID: 2026-10-07T20:20:55Z-609efeGenerated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Draft PR: #1085
Generated by Claude Code
Test rollback of an orphaned superseded copy
Restore store copies a superseded record patched