Skip to content

fix(driver-manager): stop flagging installed drivers as stale duplicates - #262

Merged
dbfx merged 2 commits into
mainfrom
fix/driver-manager-stale-detection
Jul 27, 2026
Merged

fix(driver-manager): stop flagging installed drivers as stale duplicates#262
dbfx merged 2 commits into
mainfrom
fix/driver-manager-stale-detection

Conversation

@dbfx

@dbfx dbfx commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #242.

What was wrong

scanDrivers() decided which driver packages are "stale" like this:

  1. Group every OEM package by provider + device class.
  2. Sort each group by version and call the highest one current.
  3. Also call a package current if it's bound to a device.
  4. Everything else is stale — and pre-selected, so Scan → Clean feeds it straight to pnputil /delete-driver.

Both of the signals it relied on are wrong.

provider + class is not a driver identity. Vendors routinely ship several unrelated drivers in one class — Intel's Ethernet and Wi-Fi packages are both class Net. Grouping them makes the lower-versioned one look like a stale duplicate of the higher-versioned one. parseEnumDrivers made this worse: originalName's fallback chain ended at Driver Package Provider, so when pnputil reported no original INF name, the "identity" collapsed to the vendor name outright.

"Not bound to a device" is not evidence of staleness. getActiveDriverNames() reads Win32_PnPSignedDriver, which only covers devices that are present right now. A mesh/VPN tunnel adapter only materialises a device while the tunnel is up, so its driver looks unbound whenever the client is idle. Unplugged docks, printers, phones and dongles look the same. pnputil /delete-driver only refuses a package whose device is present, so nothing downstream stopped the removal either.

Together: a working Tailscale install with its tunnel down had its Wintun package grouped against another Tailscale-provided Net package, marked stale, pre-selected, and deleted. The client can then no longer create its adapter, which is the NoState / "Tailscale is starting, please wait" loop in the issue that only a reinstall clears.

The fix

  • Group by the original INF name (wintun.inf), which is what actually makes two packages versions of the same driver. Packages that report no original name are keyed on themselves, so they can never look like a duplicate of anything.
  • Only call a package superseded when the same INF has a strictly newer package that is actually bound to hardware. A driver nothing is bound to is now left alone regardless of version. Equal, missing or unparseable versions are left alone rather than guessed at.
  • Drop Driver Package Provider from the originalName fallback chain.
  • Fail safe: if the bound-driver query fails, the anchor set is empty and the scan reports no stale packages. Previously an empty set meant "everything but the newest per vendor is stale".
  • cleanDrivers() re-queries bindings and refuses anything currently bound, since the list it acts on can be minutes old.

Note there's no vendor allowlist here. The issue suggested excluding known VPN/mesh drivers by name; that turned out to be unnecessary — under the new rule a lone Wintun package is a group of one and is never removable, and the same protection extends to every vendor rather than a hardcoded list.

Tests

New src/main/ipc/driver-manager.ipc.test.ts — 27 cases covering parseEnumDrivers, driverIdentityKey, findSupersededDrivers, scanDrivers and cleanDrivers, including the two #242 regressions (unbound tunnel driver kept; two INFs from one vendor+class not treated as versions of each other), the fail-safe path, and cleanDrivers refusing a bound package.

Full suite: 2243 passing.

Not addressed

The issue also reports ollama app.exe being deleted from %LOCALAPPDATA%\Programs\Ollama\. I traced every file-deletion path in the main process and could not find one that reaches a single executable inside an install directory without the user explicitly selecting that file: the rule-driven cleaners only touch paths declared in rules/, the leftover scanners operate on whole folders and never auto-select, and the duplicate/large-file finders require the user to choose a directory and tick individual files. Worth keeping the issue open for that half, or splitting it out — I don't want to claim a fix I can't point at.

The stale-driver scan grouped driver packages by provider + device class
and declared every package in a group except the highest-versioned one
stale, unless it was bound to a device that was present at scan time.
Both halves of that are wrong, and the scan pre-selects its results, so a
Scan then Clean pass ran `pnputil /delete-driver` over them.

Provider + class is not a driver identity. Vendors ship several unrelated
drivers in one class (Intel's Ethernet and Wi-Fi packages are both Net),
so unrelated packages were treated as one another's versions. The
originalName fallback chain ended at the provider name, which collapsed
the identity further when pnputil reported no original INF name.

"Not bound to a present device" is not evidence of staleness either.
Virtual network adapters only materialise a device while they are up, so
a mesh/VPN tunnel driver looks unbound whenever the client is idle;
removable hardware looks unbound whenever it is unplugged. pnputil only
refuses to delete a package whose device is present, so nothing stopped
the removal.

Group by the original INF name instead, keying packages that report none
on themselves so they can never look like a duplicate, and only call a
package superseded when the same INF has a strictly newer package that is
actually bound to hardware. A driver nothing is bound to is now left
alone, and a failed bound-driver query yields an empty stale list rather
than marking everything stale. cleanDrivers re-checks bindings before
removing, since the list it acts on can be minutes old.

Fixes #242

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0be855d78c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +173 to +177
export function driverIdentityKey(d: RawDriver): string {
const published = d.publishedName.trim().toLowerCase()
const original = d.originalName.trim().toLowerCase()
if (original.endsWith('.inf') && original !== published) return `inf:${original}`
return `pkg:${published}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include vendor identity in the INF grouping key

On systems where unrelated driver packages use the same original filename (for example, two vendors both shipping driver.inf), this key merges them because original INF names are not globally unique. If the higher-versioned package is bound, the other package is classified as stale and pre-selected for deletion even though it is a different driver; include provider or another package identity signal in the key rather than relying on the filename alone.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 83ca106 — the key is now inf:<original>|<provider>, and a package missing either half is keyed on itself so it can never look like a duplicate. Regression tests cover two vendors shipping driver.inf and the unknown-publisher case.

Comment thread src/main/ipc/driver-manager.ipc.ts Outdated
Comment on lines +229 to +232
// Strictly older than the bound copy. Equal, missing or unparseable
// versions compare as 0 and are left alone rather than guessed at.
if (compareVersions(anchor.version, d.version) > 0) {
superseded.add(d.publishedName.toLowerCase())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unknown versions before comparing packages

When the bound package has a valid version but another package in its group has a missing or nonnumeric version, compareVersions converts the unknown value to zero, so this condition marks that package superseded—for example, 2.0 compares greater than an empty string. The scan then pre-selects an unverified package for deletion despite the stated fail-safe behavior; validate both version strings before ordering them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 83ca106 — both the anchor and the candidate must now have a readable dotted-numeric version before they are ordered, so a package with a missing or non-numeric version is never marked superseded. Three tests added for the mixed cases; the doc comment was wrong about this too and has been corrected.

…own versions

Two gaps in the supersession rule, both from review.

The original INF name is a filename, not a globally unique id: generic
names such as driver.inf ship from more than one vendor, so keying on it
alone could still group two unrelated packages and mark the unbound one
stale. Include the publisher in the key, and fall back to keying a
package on itself when either half is missing.

compareVersions() reads an absent or non-numeric version as zero, so a
bound package at "2.0" ordered above a package whose version pnputil did
not report and marked it superseded — the opposite of the documented
fail-safe. Only order two packages when both versions are readable dotted
numbers, on both the anchor and the candidate.
@dbfx
dbfx merged commit c85e72d into main Jul 27, 2026
9 checks passed
@dbfx
dbfx deleted the fix/driver-manager-stale-detection branch July 27, 2026 03:41

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83ca106d25

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 134 to 137
originalName:
fields['original name'] ||
fields['original inf'] ||
fields['driver package provider'] ||
publishedName,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prefer enumeration that returns original INF names

On Windows versions where successful legacy pnputil -e output omits Original Name, this fallback makes originalName equal the unique oem*.inf published name. driverIdentityKey consequently assigns every package its own pkg: key, so no group reaches two entries and the scan reports zero stale drivers even when an older package was replaced. I checked the scanDrivers command order: it still runs the legacy -e form first and tries /enum-drivers only when that command fails, so successful but information-poor output is never retried; prefer /enum-drivers first or recover original INF identities before applying this fallback.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Registry/Driver/App Cleaner removed working Ollama and Tailscale components, breaking both apps

1 participant