fix(rollback): surface missing-blob aborts in the JSON envelope - #204
Merged
Mikola Lysenko (mikolalysenko) merged 5 commits intoAug 19, 2026
Merged
Conversation
When rollback aborted because beforeHash blobs were missing (offline) or undownloadable (online 404), the --json envelope was contentless and self-contradictory: exit 1 with failed:0, empty results/warnings, and no diagnostic on any stream — while human mode explained the failure fine. Confirmed on every apply leg of the 2026-08-18 pnpm matrix. - Both pre-flight abort paths now synthesize one failed RollbackResult per gated package: purl, the missing blob hash, the offline-gate or per-hash download reason, and the `socket-patch repair` hint; counters follow apply's per-package semantics (failed:N, exit 1). - The abort error string is built by cannot_rollback_error(), shared with the engine's first-blocking-file emission so the two shapes cannot drift. - The "No SOCKET_API_TOKEN set" advisory printed twice per invocation (one per internal ApiClient construction); the client is now threaded through, printing once. The envelope's snake_case partial_failure status is deliberately kept: CLI_CONTRACT.md pins rollback as a pre-v3.0 legacy shape whose snake_case status is the documented consumer discriminator, and a status-string change is rated MAJOR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 502fef0. Configure here.
…talled entries as markers
The before-blob gate ran before the installed-package match, so a manifest
entry whose package was never applied and is not on disk hard-failed the
run ('Cannot rollback: ... Before blob not found', path:"") when its blob
was missing — even though there was nothing to roll back. Found on the
2026-08-18 pnpm legacy matrix; ecosystem-independent.
- The gate now covers only installed rollback targets whose files the
engine would genuinely read the blob for (already-original/deleted/
drifted files no longer gate or trigger downloads).
- Not-installed in-scope entries surface as additive marker records in
results[]: {"purl", "path": null, "skipped": "package_not_installed"} —
never failed, never counted in rolledBack/failed, never flip the exit
code. Not-installed-only runs keep the pinned exit-0 "succeeds quietly"
contract: rollback's job is to make the tree unpatched, and a
not-installed package already satisfies that end state (deliberately
asymmetric with apply's exit-1-on-unmatched; rationale documented on
RollbackOutcome and in the tests).
- remove's delegation keeps per-package semantics; fixtures in the
remove/silent suites now install the fake package so the blob gate
still fires for a genuinely installed patched package, preserving each
test's documented intent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o-op suite global_packages_e2e's no-op helper asserted results[] empty; the marker records from the previous commit now legitimately appear there. Assert the marker shape (path null, skipped=package_not_installed, no success/error keys) instead of emptiness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sts keep their teeth remove_network's fixtures were manifest-only; under the corrected gate (blobs planned only for installed rollback targets) there was nothing to roll back, so the online test observed no fetch and the offline test succeeded. Installing the package at the patched bytes makes the nested rollback genuinely need the beforeHash blob, preserving both tests' documented intent (online fetches exactly that blob; --offline fails closed without touching the network). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ollback was skipped not-installed Review follow-up: a package that is installed but missed by the crawler (layout gaps have precedent) reads as a benign not-installed skip; the remove delegation then dropped the manifest entry AND swept its beforeHash blobs — permanently destroying revert data while patched bytes remained on disk, with no machine-visible signal. remove now records an additive Skipped/rollback_not_installed envelope event (naming the purl, details.beforeBlobsRetained, plus a stderr warning) for each dropped entry whose nested rollback was skipped as not-installed, and pins those entries' beforeHash blobs into the cleanup sweep's keep set so a later repair / rollback --one-off can still restore. Standalone rollback's not-installed-exit-0 contract and --skip-rollback semantics are unchanged (both pinned). RED-verified: pre-fix, the new test observed the before-blob swept (blobsRemoved:2, no event). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Aug 19, 2026
…ouch trustLockfile, takeover reconciliation, revert guards (#213) * feat(pnpm): full pnpm 7-12 vendor+hosted support — legacy lock grammars, zero-touch trustLockfile, takeover reconciliation, revert guards Built and verified against real corepack-pinned pnpm 7.33.5 / 8.15.9 / 9.15.9 / 10.34.5 / 11.22.0 / 12.0.0-rc.7 (plus legacy 1-6 probes) in the 2026-08-18 e2e campaign. HOSTED, pnpm 7/8: the v5.4/v6.0 refusal is replaced by a real rewrite — every instance key of a dep is spliced (v5 /name/ver and _peer-suffixed, v6 (peer)-parenthesized; each owns its resolution), one ledger edit per instance; a post-splice residual detector refuses the dep set-wide if any instance shape the splice regex cannot claim remains (no partial rewrites). Frozen installs from empty stores land patched bytes on both majors; tamper fails ERR_PNPM_TARBALL_INTEGRITY. HOSTED, pnpm 11/12 zero-touch: rewriting a v9 root lock now auto-writes trustLockfile: true into pnpm-workspace.yaml (create with scaffold or byte-preserving append; ledger-recorded as redirect_pnpm_workspace_trust; --no-trust-lockfile-config opt-out; only ErrorKind::NotFound creates — an unreadable existing file falls back to guidance, never overwrite; re-scans heal a missing config on already-redirected locks). pnpm 11.22 and 12-rc frozen installs succeed with no flags and no CI changes; 9/10 ignore the key (verified); the sha512 pin still fails closed under trust. Warnings name the actual spliced host (userinfo stripped) and both per-major error codes, and pre-empt pnpm 12's own rebuild-the-lock advice that silently unpatches. VENDOR, pnpm 7/8: new pnpm-legacy backend (flavor-stamped so older binaries fail closed) — package.json pnpm.overrides + legacy lock surgery emitting exactly what those majors serialize (byte-stable under pnpm's own re-lock). pnpm <= 8 absolutizes file: specifiers, so frozen installs are path-bound: surfaced as vendor_pnpm_legacy_absolute_specifier, with plain `pnpm install --offline` as the moved-checkout path (marker bytes verified). Windows-shaped canonical paths are normalized (verbatim prefix stripped, forward slashes). CONVERSIONS + SAFETY: vendoring over a hosted-redirected npm-family purl now reconciles the redirect ledger (artifact-uuid-anchored matching — version-exact, v5 underscore keys claimed; degraded ledgers keep edits fail-closed), firing vendor_supersedes_redirect exactly once; vendor --revert byte-restores the hosted lock from the wiring originals. All six npm-family vendor backends refuse to delete an artifact the live lock still references when a repair-reconstructed entry has no wiring (vendor_wiring_unknown_revert_blocked — the revert-brick fix); repair stamps detected flavors and preserves corrupt artifacts when no rebuild source exists. Legacy-era diagnostics: shrinkwrap.yaml projects get pnpm-flavored no-lockfile guidance and join the lockfile-only supplement; vendored lock entries get redirect_pnpm_entry_vendored instead of entry-not-found. Tests: e2e_redirect_pnpm_build.rs (new hosted capstone: pnpm 7-11 real corepack legs incl. the zero-touch pnpm 11 proof, tamper negative, hermetic v5/v6 legs), e2e_vendor_pnpm_build.rs ladder (@9/@10/@11 + real pnpm 7/8 lifecycle legs), takeover/reconciliation/guard/heal unit + e2e suites — all RED-verified where behavior changed. CLI_CONTRACT.md and docs/ecosystems.md updated. Stacked on #203 (lock-inventory legacy grammars); trivially overlapping test hunks with #204/#208 carry identical content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pnpm-legacy): moved-checkout recovery needs --no-frozen-lockfile (pnpm defaults frozen on under CI) CI caught what local runs could not: pnpm turns --frozen-lockfile ON when CI=true, and the pnpm <= 8 moved-checkout recovery works precisely by re-resolving the path-bound absolute specifier — frozen semantics skip that re-resolution (pnpm 8: ERR_PNPM_OUTDATED_LOCKFILE; pnpm 7: stale-path install). The lifecycle legs' recovery step now passes --no-frozen-lockfile explicitly, and the vendor_pnpm_legacy_absolute_ specifier remedy (warning text, module doc, CLI_CONTRACT.md, docs/ecosystems.md) recommends `pnpm install --offline --no-frozen-lockfile` so real CI users get working advice. Verified: the full capstone (10 legs incl. real pnpm 7/8 lifecycles) passes under CI=true locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(pnpm-legacy): lock oracle uses the real path normalizer (Windows byte-exactness) Windows CI proved the production normalizer right and the test oracle wrong: the hermetic splice legs built their expected absolute specifier with raw canonicalize().display() — the \\?\C:\ verbatim form the normalizer exists to strip. normalize_canonical_root is now pub and the oracle consumes it at both assertion sites, so the expected string is built by the same transformation the backend writes and cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(pnpm-legacy): in-file oracles also use the shared root normalizer Windows CI surfaced the same oracle-drift bug in the module's own unit tests: the fixture helper handed raw canonicalize().display() (verbatim \\?\C:\ form) to the ROOT_TOKEN substitution, the no-leak contains probe, and the moved-checkout fixture builder. All three now go through a canon_root_str() helper built on normalize_canonical_root, so every oracle spells the root exactly as the splice writes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
August 19, 2026 14:56
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Aug 19, 2026
Mikola Lysenko (mikolalysenko)
merged commit Aug 19, 2026
1e2b1e9
into
main
398 of 404 checks passed
Mikola Lysenko (mikolalysenko)
deleted the
fix/rollback-json-diagnostics
branch
August 19, 2026 15:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
When
rollbackaborted because beforeHash blobs were missing (--offline) or undownloadable (online 404), the--jsonenvelope was contentless and self-contradictory: exit 1 with{"status":"partial_failure","rolledBack":0,"failed":0,"warnings":[],"results":[]}—failed: 0on a failed run, no error field, nothing on stderr. Human mode explained the failure fine. A JSON consumer learned nothing. (Found on every apply leg of the 2026-08-18 pnpm e2e matrix; the bug is ecosystem-independent.)Two smaller issues rode along: the "No SOCKET_API_TOKEN set" advisory printed twice per invocation (once per internal ApiClient construction), and the abort error string hand-copied the engine's first-blocking-file shape.
Fix
RollbackResultper gated package: purl,filesVerifiedrecords with the engine's existingmissing_blobstatus naming the missing hash, the offline-gate or per-hash download reason, and thesocket-patch repairhint. Counters follow apply's per-package semantics (failed: N, exit 1). Created-by-patch sentinels (empty beforeHash) are excluded; output is purl-sorted. No new JSON keys — only when the documentedresults[]/failedfields are populated changed.cannot_rollback_error()shared with the engine's emission site, so the two shapes cannot drift.Deliberately not changed: the snake_case
partial_failurestatus. CLI_CONTRACT.md pins rollback as a pre-v3.0 legacy envelope whose snake_case status is the documented consumer discriminator, and rates a status-string change MAJOR — the tracked fix is the unified-envelope migration.Verification
failed:1; human-mode parity; token note printed exactly once. Unit tests for the abort-result synthesis.rollback_invariants,cli_rollback_silent,cli_parse_rollback,remove_invariants,remove_rollback_api_overrides).failed:1, exit 1.🤖 Generated with Claude Code
Note
Low Risk
Rollback CLI/JSON diagnostics only; no changes to patch application, auth, or blob storage semantics beyond populating existing envelope fields on abort paths.
Overview
Fixes
rollback --jsonwhen the run exits before the rollback loop because beforeHash blobs are missing (--offline) or still missing after a failed download. Previously that path returned exit 1 withpartial_failure,failed: 0, and emptyresults[]while stderr was muted under--json—machine consumers got no package, hash, or remedy.Both pre-flight abort paths now call
missing_blob_abort_results, which emits one failedRollbackResultper gated package (purl-sorted, emptybeforeHashsentinels skipped):filesVerifiedwithmissing_blob,targetHash, offline or per-hash download reasons, andsocket-patch repairhints;failedmatches apply’s per-package semantics.cannot_rollback_errorin core centralizes theCannot rollback: {file} - {why}string so synthesized aborts match mid-run engine failures.rollback_patches_inneraccepts the telemetryApiClientfromrun()so blob fetch does not build a second client (dedupes the No SOCKET_API_TOKEN advisory); theremovedelegation still builds on demand whenNone.Tests lock the new envelope shape, human-mode parity, and client-notice count.
Reviewed by Cursor Bugbot for commit 502fef0. Configure here.