You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
[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.lock to 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 # ORIGINAL
remove 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 next bun 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, in superseded_record_skip: mismatched is 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 with holds_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 skip continues past the restore for the whole target, so the orphan's ready file is never rolled back. The purl then goes into superseded, 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.
[agent] Triage: priority:p1 (Bun). Confirmed on main 05ecc6e: in crates/socket-patch-cli/src/commands/rollback.rssuperseded_record_skip sets mismatched when any verified file is a hash mismatch, and only the Gradle branch checks holds_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.
[agent] Claiming this issue (shared root cause: superseded_record_skip skips 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-609efe
[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 andremoveskip 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 linksnode_modules/plainpkgto a new store entry (node_modules/.bun/plainpkg@http+++…bbbbbbbb…) with B's bytes. It never removes the oldnode_modules/.bun/plainpkg@1.0.0entry (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:
bun.lockto the registry tuple,.socket/manifest.json,success.The advised
bun installthen prints "no changes" and relinksnode_modules/plainpkgto the orphanedplainpkg@1.0.0entry. The project now runs agent patch A with no manifest record and no blobs. A secondrollbackis asuccessno-op, so socket-patch can't undo it any more. Onlybun install --force(or deletingnode_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-lockfilefrom 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.0as uuid A, later superseded by uuid B)remove pkg:npm/plainpkg@1.0.0instead ofrollbackgives the same end state: exit 0, then the advisedbun installrelinks 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 followingbun installlinks ORIGINAL bytes. So the crawler does see the orphan. The superseded skip is what drops it.Expected vs actual
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 install→ rollback →bun installremovebun.lock)bun.lockb)git checkout -- bun.lockbremedymacOS/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 main05ecc6e.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/.bunorphans) and #764 (the hoisted in-place reinstall keeps patched bytes). #1009's--forceadvisory for #764 would hide the symptom but not the dropped record.