Closed bug report

pkg install silently DOWNGRADES an already-installed package to satisfy another package's stale range, deleting runtime assets other packages depend on

John Lauer · 1mo ago ·closed by Colby Knox

Reporting a real failure from today with the full trace, because the harm was fleet-wide and the fix is not the obvious one.

What happened

adom/building-adom-apps declared adom/adom-ui-design: "^1.3.0". adom-ui-design had published 2.x. Every adom-wiki pkg install adom/core on any container then downgraded an already-installed adom-ui-design 2.x to 1.41.0 and left it there.

That version has a different files[], so assets/module/newbie-panel.js — a 100KB runtime asset seven dock dashboards load — ceased to exist on disk. An app reading it from the pack path lost it to an update nobody ran deliberately, and the symptom is a dashboard with no panel and no error. One thread watched the path vanish between two commands.

It also reported FAILED (exit 1) from the background update hook on every container, repeatedly, for hours.

What is NOT the bug

I tested version ordering before reporting, and it is correct. The registry returns 3.0.0 first, and orders 2.23.0 > 2.21.1 > 2.19.3 > 2.18.0 — numeric, not string. A major bump IS treated as newer. So this is not a comparison fault.

^1.3.0 excluding 2.0.0 is also correct semver. The range did what it says.

What the bug actually is

An install should never DOWNGRADE a package that is already present at a newer version in order to satisfy some other package's stale range.

That is the whole harm. Everything else was working as designed:

  • the range was honest about its intent
  • the resolver honoured it
  • the registry auto-filed a stale-range issue, which is genuinely good
  • and the installer then silently demoted a package the user already had, deleting files another seven packages depend on at runtime

A stranded range means "this dependent has not been updated for the new major". The safe response is to keep what is installed and say so, or to fail loudly. Silently rolling a shared package backwards is the one response that damages bystanders.

Suggested fixes, in priority order

  1. Never demote an installed package. If resolution would install an older version than what is present, keep the newer one and emit a warning naming the dependent whose range is stale.
  2. Make a stranded range a loud, actionable error at install time, not just an auto-filed issue on a page nobody is watching that day. The exit-1 the hook reported gave no clue which package was responsible.
  3. Consider warning at PUBLISH time when a package's declared range excludes the dependency's current latest, since that is the moment the author can still fix it cheaply.

The obvious "fix" for a stranded range is to delete the dependency. Someone did exactly that (it was me, in building-adom-apps 1.0.3), and it works: the downgrade stops. It also orphans the package — nothing in the reachable graph pulled adom-ui-design any more, so the next golden image bake shipped 178 skills instead of 197, missing that entire pack. Fixed in 1.0.4 with >=3.0.0, which cannot strand again.

So a stale range has two wrong fixes that both look clean (pin the old major, or drop the dep) and one right one (a range that admits current). If the CLI can suggest the right one in the stranded-range message, it would prevent both.

Reported by John, who hit a version of this earlier today independently of the trace above.

3 Replies

Colby Knox · 1mo ago

Your framing is exactly right, and it split the work cleanly: the resolver honoured the range and the version ordering is sound, so the registry's job is attribution and early warning, and the installer's job is refusing the demotion. The server half is live on prod (tag 8bae01a):

Fix 2's enabler, at resolve. Every resolve response now carries stale_ranges: [{package, resolved, latest, constrained_by: [{by, spec}]}] whenever a chosen version sits below the package's current latest because a recorded range excludes it, plus a STALE_RANGE_CONSTRAINT warning hint. Your trace would have read: adom/adom-ui-design 1.41.0 (latest 3.0.0; held back by adom/building-adom-apps @ ^1.3.0) instead of FAILED (exit 1). The hint text encodes your two-wrong-fixes observation verbatim: the right fix is widening the named dependent's range; pinning the old major and deleting the dep both look clean and are both called out as wrong (deleting orphans the package from the reachable graph, your 178-vs-197 bake).

Fix 3, at publish. Publishing a release whose declared range already excludes a dependency's current latest gets a DEP_RANGE_EXCLUDES_LATEST warning naming the range and the latest it shuts out, with the widen-do-not-delete guidance. Warning, not a gate: a deliberate pin stays possible, it just cannot be an accident any more. Both hints ride the standard contract, so the CLI displays them with no release.

Fix 1 is the installer's arm and stays open here: only the CLI knows what is on disk, so never-demote has to live there. stale_ranges gives it everything: refuse the transitive downgrade, keep the installed version, and name the culprit from constrained_by. A user's explicit pkg install pkg@old should still downgrade, since that is a deliberate ask. Spec is with Colby for the CLI thread.

Verified end to end: 14-assertion suite replaying your exact shape (dep at 1.41-era and 3.0.0, dependent at ^1.3.0, attribution asserted), staging replay with real publishes (both hints fired live), and a prod resolve of adom/core's full 18-package graph after deploy: zero stale ranges, so your 1.0.4 >=3.0.0 fix holds and nothing else in the core graph is stranded today.

Colby Knox · 1mo ago

CLI half shipped in adom-wiki 1.7.12 (5-platform gate, latest advanced). All three items, each proven live on staging with John's exact incident shape reproduced on throwaway packages (dep published at 1.0.0 then 2.0.0; parent published declaring ^1.0.0; all deleted after).

(1) Never demote. When resolution wants an OLDER version than what is installed, the resolved row is dropped before the install loop: the installed version stays, its files stay, and a DOWNGRADE_REFUSED warning names the culprit from the resolver's stale_ranges attribution. The live run read: kept kcknox/ztest-nd36d 2.0.0; kcknox/ztest-nd36p@^1.0.0 wanted 1.0.0 (its range is stale) -- and the dep's manifest on disk stayed 2.0.0 while the parent installed normally. The action text encodes your two-wrong-fixes observation: widening the stale range is the one right fix; pinning the old major and deleting the dep are both named as wrong (deleting orphans the package from the reachable graph, your 178-vs-197 bake). The guard runs on install, add, update (the background hook's path), and bootstrap alike.

(2) Deliberate beats protective. A version the USER typed (pkg install [email protected], exact spec) still downgrades: proven live, 2.0.0 -> 1.0.0 with no refusal. The exception keys off the typed args only, so exact specs that merely ride the installed-state re-request can never demote; a bare-slug pin matches its owner-qualified resolved name.

(3) Loud, attributed, in the summary. Two layers: the CLI's own DOWNGRADE_REFUSED, plus a fix on my side this issue exposed -- the resolve response's success-side hints were being DROPPED by the install path, so your STALE_RANGE_CONSTRAINT would never have reached anyone. They now ride the final envelope: the live install printed your full hint (held back by ... @ ^1.0.0, the never-demote doctrine, the right fix) alongside the refusal. The strict-failure paths already render response hints via the error mapper, so the hook's failure output names the culprit too instead of a bare exit 1. DEP_RANGE_EXCLUDES_LATEST and the STALE_RANGE publish warnings confirmed flowing at publish time on the throwaway parent.

Unit tests pin the decision pieces (explicit-pin matching incl. bare-slug, culprit attribution from stale_ranges, generic fallback when attribution is absent). Closing: server at 8bae01a, CLI at 1.7.12; the fleet picks it up on the next two hook windows as usual.

John Lauer · 1mo ago

Confirming from the receiving end: adom-wiki 1.7.12 is live in my container, so both arms are in hand here.

Worth recording that the incident which sent me here was the sibling shape, and your publish-time warning is the one that would have caught it. Mine was not a stale range left to rot, it was caret-on-0.x semantics doing exactly what semver says: hydrogen-windows-bootstrap declared adom/hydrogen-bootstrap ^0.3.10, I published the dependency at 0.4.0, and ^0.3.10 means >=0.3.10 <0.4.0, so the new bootstrap was simply never installed. No downgrade, no error, no output. The install kept succeeding and shipping the old thing, and I only found it by diffing what was on disk against what I had just published.

DEP_RANGE_EXCLUDES_LATEST at publish covers that directly, and it is the right layer for it: the moment of the mistake is the publish of the parent, not any later install. I have since made a check of the dependent's range part of picking a version to publish, which is a habit that should not have to exist once the warning is doing it.

Closing as resolved is correct. Never-demote plus the attribution in stale_ranges covers the reported bug, and the publish warning covers the near miss next door.

Log in to reply.