Skip to content

Fix/review follow ups - #153

Merged
kjlubick merged 16 commits into
developfrom
fix/review-follow-ups
Oct 8, 2026
Merged

kjlubick merged 16 commits into
developfrom
fix/review-follow-ups

Conversation

@Noah-Tervalon-Nvidia

Copy link
Copy Markdown
Collaborator

Changelog title

Safer engine removal and data resets

Changelog body

  • Removing an engine never follows a symlink or Windows junction into another folder.
  • An LM Studio models folder linked from ~/.lmstudio/models is kept when LM Studio is removed.
  • Resetting app data, from Settings or the terminal interface, stops before deleting anything if an engine PAIR installed cannot be removed, and says which one.

Bumps

  • services: patch
  • nvpair-cluster-manager: none
  • nvpair-engine-manager: patch
  • nvpair-errors: none
  • nvpair-job-scheduler: none
  • nvpair-manual-nodes: none
  • nvpair-node-info: none
  • nvpair-node-scanner: none
  • nvpair-node-settings: none
  • nvpair-proxy: none
  • nvpair-tui: patch
  • nvpair-ui-broker: none
  • nvpair-workload-manager: none

Summary

Review fixes for the work below this in the stack. Stacked on
fix/indeterminate-install-progress (#145), at the top of #147 → #148 → #117 →
#150 → #145; merge it after them, and before the stack ships.

Engine removal (#150)

  • Junctions are never followed. Since Go 1.23 a Windows junction is reported
    as an irregular file, not a symlink, so the symlink check let the elevated
    uninstaller read through one and empty the directory it names. Removal now
    descends only into what Lstat reports as a real directory. Two Windows
    tests use real junctions; CI runs Go tests on Linux only, so they run on
    Windows machines.
  • The model store is matched by file identity. A store that is a symlink to
    another folder under the removal target was deleted, and case handling
    followed the OS rather than the filesystem. os.SameFile replaces both. New
    tests cover a symlinked store, a store two levels down (the first to exercise
    the recursion), and a store spelled in another case.
  • spec.md now states what removing an engine may delete.

Resets (#150)

The TUI and desktop resets wiped app data even when an engine could not be
removed, deleting the only record that it was PAIR's. Both now stop first and
say which engine, or that the service is not running. The orchestrator gains its
first tests.

Smaller fixes

Test plan

  • go vet (darwin, linux, windows) and go test ./... in
    nvpair-engine-manager and nvpair-tui.
  • npm run typecheck, npm run lint, npm run test:unit from desktop/.
  • TestUninstallTerminatesRunningInstance fails locally on macOS identically on
    develop. The Windows junction tests compile but did not run here.

@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia added this pull request to stack #149 October 8, 2026 12:47
@kjlubick
kjlubick self-requested a review October 8, 2026 15:56

@kjlubick kjlubick left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM with small changes

Comment thread services/nvpair-engine-manager/modelsdir_windows_test.go
Comment thread services/nvpair-engine-manager/removepath.go
Comment thread services/nvpair-engine-manager/catalog_test.go Outdated
Comment thread services/nvpair-engine-manager/main.go Outdated
@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia force-pushed the fix/review-follow-ups branch 2 times, most recently from bb0596a to beb4b7c Compare October 8, 2026 17:03
Base automatically changed from fix/indeterminate-install-progress to develop October 8, 2026 17:18
removeTreePreserving unlinked a symlink rather than descending into it,
so an uninstall could not be steered into another tree. It tested for
ModeSymlink. Since Go 1.23 a Windows junction is reported as an
irregular file instead -- neither a symlink nor a directory -- so a
junctioned remove target, or a junction on the way to the model store,
was read with ReadDir and the directory it names emptied.

The Windows uninstaller runs this elevated, and both a junction and the
install marker can be made without privilege, so a standard user could
aim an administrator's "remove all data" at a protected directory.

Descend only into what Lstat reports as a real directory and unlink
everything else. The test that called itself the junction case created
a symlink, which Go reports differently; two Windows tests now use real
junctions. CI runs Go tests on Linux only, so they run on Windows
machines rather than in CI.

Signed-off-by: Terve <ntervalon@nvidia.com>
The model store was matched by comparing path strings, folded for case
on Windows and macOS. Two consequences:

- A store that is a symlink lost its models. With ~/.lmstudio/models
  linking to ~/.lmstudio/weights, removing ~/.lmstudio kept the link and
  deleted weights as an unrelated sibling.
- Case handling followed the operating system rather than the
  filesystem. A macOS volume can be case-sensitive, and a Linux mount
  can ignore case, so folding by GOOS could both keep and delete the
  wrong directory.

Compare files instead (os.SameFile): the store is the path as named and
the directory it resolves to, and the directories above it are taken
along both the written and the resolved path. The filesystem then says
what is the store, whatever the spelling.

pathWithinRoot goes back to the exact comparison it had before case
folding was added; its remaining callers compare paths they built
themselves. Manifest validation now compares without case on every
platform, since one manifest runs on filesystems of both kinds.

Tests cover a symlinked store, a store two levels down (the first test
to exercise the recursion), and a store spelled in another case, which
runs only where the filesystem ignores case.

Signed-off-by: Terve <ntervalon@nvidia.com>
"Reset all data and quit" asked the engine manager to remove the engines
PAIR installed, then wiped the data directory whatever the answer. The
wipe deletes the install markers -- the only record that an engine is
PAIR's -- so an engine that could not be removed, LM Studio in the
user's home most of all, was left on disk with nothing allowed to remove
it afterwards.

Wipe only once every engine PAIR installed is gone. If one could not be
removed, or the engine manager could not be asked, the reset stops
before deleting anything, names the engine on the status line, and gives
the keyboard back so the operator can try again or uninstall it first.
An engine PAIR did not install is still skipped, not failed.

Signed-off-by: Terve <ntervalon@nvidia.com>
…ll on disk

Settings > Reset app data treated engine removal as best-effort: a
failed removal, a failed call, or a service that was not running were
logged, and the app data folder was deleted anyway. That folder holds
the install markers, the only record that an engine is PAIR's, so the
engine that could not be removed was left behind for good.

Stop before the teardown instead, with an error the Settings card
already shows: which engine could not be removed, or that the service is
not running. The confirmation now says a reset can stop.

The orchestrator had no tests. These cover the wipe going ahead once
every engine PAIR installed is gone, and stopping, with nothing torn
down, for each of the three ways removal can fail.

Signed-off-by: Terve <ntervalon@nvidia.com>
The uninstall changes added a progress line to the Windows uninstaller, an engine-manager error a user reads when an uninstall is declined, and comments in the uninstallers, all naming the product "Personal AI Router". The app is NVIDIA PAIR everywhere a user sees it. The data folder and the Windows firewall rule names keep "Personal AI Router": those are an on-disk path and stable identifiers, not branding.

Signed-off-by: Terve <ntervalon@nvidia.com>
Review asked for the expected error to be stated rather than searched for. The over-long query's error is a fixed sentence, so the test now compares it whole. The failed search's error ends in the operating system's connection error, which names a random port, so it cannot be; the test now checks the one thing that has to hold, that no *url.Error -- the error that carries the search URL -- survives in the chain.

Signed-off-by: Terve <ntervalon@nvidia.com>
…dels folder

Getting started now says what to do when Restart & install finishes the update from PAIR.app to NVIDIA PAIR.app without reopening the app, and names the app NVIDIA PAIR in the macOS install step, which still said PAIR after the rename.

Engine lifecycle now states the one case where removing LM Studio deletes models: a models folder the user moved elsewhere inside ~/.lmstudio. PAIR preserves the engine's default store, and since the store is now recognised by identity, one linked back to ~/.lmstudio/models is kept.

Signed-off-by: Terve <ntervalon@nvidia.com>
The spec said model stores live outside the data folder, but not how an engine's removal treats them. State it: only the paths uninstall.remove names are deleted, the store is matched by file identity rather than path, links and junctions are unlinked rather than followed, and a target that is the store or inside it is refused.

Signed-off-by: Terve <ntervalon@nvidia.com>
…er chose

--uninstall-managed builds its registry from the bundled manifests only,
so a user-writable override cannot choose what an elevated uninstaller
runs or deletes. That also dropped the user's port. Before removing an
engine, Uninstall checks its port to stop the instance it owns or to
refuse a foreign service there; with the default port in hand it looked
in the wrong place for an engine the user had moved, and could refuse
outright if something else answered on the default.

Take the port, and only the port, from each override. A port decides
where the uninstaller looks; what it may stop is decided by the managed
binary's path, which stays bundled, and so do the commands and the
paths it deletes.

Signed-off-by: Terve <ntervalon@nvidia.com>
…deleted

The only failure case for UninstallManaged declared no removal targets, so it never reached a removal that deletes some files and fails on others -- the case the loop's continue exists for. The new test makes one engine's binary undeletable: that engine reports a failure, keeps its install marker, loses what could be deleted and keeps its models, and the engine after it is still removed.

Signed-off-by: Terve <ntervalon@nvidia.com>
…able

Review asked whether curl's exit codes differ across platforms. They do not: 22 is curl's own code for an HTTP error under --fail. On Windows exec.Command runs curl.exe, which ships with the OS, not PowerShell's curl alias, and a host with no curl at all gets a start error rather than an exit code, which the test already skips.

Signed-off-by: Terve <ntervalon@nvidia.com>
The README and the engine lifecycle page said removing PAIR, or resetting its data, removes the engines PAIR installed. A reset now stops and says which engine it could not remove, but a platform uninstaller cannot stop halfway: it leaves that engine in place and finishes. Say so, and where to remove it from.

Signed-off-by: Terve <ntervalon@nvidia.com>
…age does

The _windows suffix already limits the file to Windows builds. Every other OS-specific test in the package also states it with a //go:build line, so this one does too.

Signed-off-by: Terve <ntervalon@nvidia.com>
Only a real directory can lead to the model store, so under now returns false for anything else rather than relying on its caller to check first.

Signed-off-by: Terve <ntervalon@nvidia.com>
…arately

One condition checked both that there was an error and what it said. Split, the test fails plainly if the query is answered, and compares the message only once there is one -- comparing a nil error's message would panic instead of failing.

Signed-off-by: Terve <ntervalon@nvidia.com>
The uninstaller took only the port from a user's override. The running
engine manager reads the whole override, so a reset kept a models_dir
the user had pointed elsewhere while the uninstaller kept only the
bundled default -- and removed the user's store if it sat inside a
removal target.

Take models_dir along with the port. Both say where the engine's things
are rather than what to run or delete: a store decides only what
removal keeps. The overlay is checked with the same validation startup
applies, so a store that would hold a removal target, or a templated
one, is ignored whole.

Signed-off-by: Terve <ntervalon@nvidia.com>

@kjlubick kjlubick left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@kjlubick
kjlubick merged commit 367c080 into develop Oct 8, 2026
13 checks passed
pair-release-intent Bot added a commit that referenced this pull request Oct 8, 2026
Apply release intent from PR #153.

Applies-PR: #153
@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia deleted the fix/review-follow-ups branch October 8, 2026 18:12
Noah-Tervalon-Nvidia pushed a commit that referenced this pull request Oct 9, 2026
…reenshots (#151)

## Description

A documentation pass over the release that landed in `develop` with
#147, #148, #117, #150, #145, and #153. It covers llama.cpp everywhere
the docs list engines, replaces the terminal-interface screenshots of
the old ten-tab interface, and corrects docs that no longer match the
code.

The branch was replayed onto `develop` after the stack was
squash-merged, so it carries only the docs commits.

<!-- pair-release-intent:v1 -->
### Changelog title
n/a

### Changelog body
n/a

### Bumps
- services: none
- nvpair-cluster-manager: none
- nvpair-engine-manager: none
- nvpair-errors: none
- nvpair-job-scheduler: none
- nvpair-manual-nodes: none
- nvpair-node-info: none
- nvpair-node-scanner: none
- nvpair-node-settings: none
- nvpair-proxy: none
- nvpair-tui: none
- nvpair-ui-broker: none
- nvpair-workload-manager: none
<!-- /pair-release-intent:v1 -->

## Scope

**Terminal interface screenshots**
(`docs/assets/onboarding/terminal-interface/`)

- The three existing images showed the ten-tab interface from before
#117. They are replaced with renders of the current Nodes, pairing, and
Jobs views, at the same size as the originals.
- New `04-tui-node-detail.png`: a machine's Engines and Models panes
with Ollama, LM Studio, and llama.cpp. It is placed in "Look at One
Machine".
- The renders drive the real `ui` views and styles with sample data for
two machines, so the layout, columns, and footer are the interface's own
output. The harness that produced them is not part of this pull request.

**Terminal interface capabilities**

- `docs/known-issues.mdx` and `docs/engine-lifecycle.mdx` said the
terminal interface cannot list or delete models, change ports, manage
another node's engines, or show which node ran a job. It does all of
these now. The lists keep only what is still true: updating an engine,
sending your own inference request, installing PAIR updates, and
surviving a dropped connection.
- `docs/engine-lifecycle.mdx`: ports and startup arguments can be edited
from a paired machine. Only uninstall, update, and CORS settings stay
local.
- `README.md`: leaving a cluster is `l` on the **Nodes** tab. There is
no Cluster tab any more.
- `docs/architecture.mdx`: the broker's `ping` is used by the
**Service** tab, not a health view.

**Terminal interface docs** (`docs/terminal-interface.mdx`,
`services/nvpair-tui/README.md`), checked against the code:

- It runs a broker and ten workers, not eleven.
- While the interface runs, its own logs go to the Logs tab, not stderr.
- The engine port, proxy port, and startup arguments are offered on a
peer and written through `engine:apply-settings`. Only restart and
uninstall are local-only. A peer's screen lists only its engine port,
though `p` still changes that peer's proxy port.
- Only a plain `go build` skips the update check. Builds made with
`services/build.sh` or `services/build.bat` check.
- `r` forgets a machine added with `f` without asking, and the "asks
first" rule says so. The catalog filter also matches tags and clears
with `c`.

**llama.cpp coverage**

- `README.md`: removed the claim that first-run setup does not preselect
llama.cpp. `WELCOME_ENGINE_DEFAULT_SELECTED` preselects it. The macOS
install step names **NVIDIA PAIR**.
- `docs/getting-started.mdx`:
- "Using an Engine's Own Command Line" gives llama.cpp's install path on
Windows, Linux, and macOS, its cache, and querying its router on `8081`.
The `OLLAMA_HOST` advice is marked as Ollama-only.
  - Ollama's path table gains a macOS row.
- The endpoint table gains a llama.cpp row: OpenAI-compatible paths on
`8080`, no Ollama `/api/...` and no Anthropic Messages API.
- `docs/troubleshooting.mdx`: a `llama-server` you started yourself on
`8080` hides requests from PAIR. The fix is to stop it and reopen PAIR,
because the proxy keeps its fallback port until the broker restarts.
- `docs/building.mdx` and `docs/engine-lifecycle.mdx`: model folders and
example ports include `~/.llamacpp`, `~/.lmstudio/models`, and `8080`.
- `services/installer/linux/INSTALL.md` and
`services/installer/macos/INSTALL.md`:
- Models live in each engine's own store, so deleting the bundle or
configuration leaves them.
- The bind-conflict note named `11435`, which is Ollama's relocated
engine port, not a proxy port. It now lists the three proxy ports.
- Linux: the bundle layout lists `nvpair-tui`, and the requirements say
`x86_64` or `arm64`, which `installer_build.sh` produces.
- Service and developer references that still assumed two engines:
  - the `nvpair-proxy` README and spec;
- the `nvpair-job-scheduler` spec, including the `scheduler:get-status`
shape;
  - the `nvpair-workload-manager` README and spec;
  - the `llamacpp-proxy` error IDs in `nvpair-errors`;
  - the `nvpair-ui-broker` README;
  - `.cursor/rules/proxy-inference-routing.mdc`;
  - `desktop/docs/services-backend.md`.
- `services/nvpair-engine-manager/README.md` lists
`engine:uninstall-managed`. `desktop/docs/services-parity.md` marks the
pull `percent` as optional, after #145.

**Not included**

- Desktop screenshots that still show only Ollama, such as
`getting-started/01-first-run-setup.png`. They need a capture from the
Electron app.
- `CHANGELOG.md`, which release-intent automation writes.

## Validation

- Swept every `*.md`, `*.mdx`, and `.cursor/rules/*.mdc` file for engine
lists that name Ollama or LM Studio without llama.cpp, and for
references to the old terminal interface. Each hit was checked against
the code. Statements that PAIR adopts an existing Ollama or LM Studio
are left alone, because llama.cpp is never adopted.
- Each factual change was checked against its source, including the
engine manifests, `engines.All()`, `schedule.go`, `engineproxy.go`,
`nodedetail.go`, `nodes.go`, `logoutput.go`, `updatecheck.go`,
`services/build.sh`, `installer_build.sh`, and `appdir.go`.
- A three-model review pass over the branch. Every finding was verified
against the code before it was applied.
- Reviewed each rendered screenshot against the doc text and alt text it
accompanies.
- `go vet ./ui/` in `services/nvpair-tui` after the harness was removed.
- `python3 scripts/release-intent/validate_pr.py --description-file
<this description> --skip-owned-files-check`

## Risk

Documentation and images only. No code, build, or versioned file
changes.

Found while checking and left for a separate change:

- In a colour terminal, an unplaced job's RAN ON cell renders as `c…`
instead of `choosing`. The styled text is truncated by its byte width,
and the tests render without colour, so they do not see it.
- A comment in `ui/updatecheck.go` still says "eleven workers".
- `nvpair-manual-nodes` probes hand-added nodes for Ollama and LM Studio
only, not llama.cpp.
- `installer_build.sh` does not copy `inference-dispatcher` into the
services tarball, so the Jobs tab's test traffic cannot start from it.

## Checklist

- [x] I have read the [Contributing
Guidelines](https://github.com/NVIDIA/Personal-AI-Router/blob/main/CONTRIBUTING.md).
- [x] Every commit is signed off (`git commit -s`), certifying the
[Developer Certificate of Origin](https://developercertificate.org/).
- [ ] New or existing tests cover the change. (Documentation only.)
- [x] Relevant documentation is updated.
- [x] I checked the diff, changed filenames, and commit messages for
credentials, private data, internal URLs, internal issue identifiers,
and generated artifacts.
- [x] I recorded the validation commands and results above.
- [x] I declared version bumps in the release-intent block above.
`services/versions.json` is written by automation — do not edit it by
hand.

---------

Signed-off-by: Chris Kelsey <ckelsey@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants