Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions desktop/docs/services-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ history.
| Process supervision | Complete | Electron starts only `nvpair-ui-broker`; the broker supervises all workers |
| Discovery | Complete | Broker discovery snapshots drive available nodes and node state |
| Node telemetry | Integrated with direct poll | Electron polls advertised `/v1/node-info` (plain HTTP); remote OS and some remote telemetry are backend-limited |
| Manual nodes | Complete with local persistence | Broker owns probing and proxy registration; Electron persists entries for replay |
| Manual nodes | Complete with local persistence | Broker owns probing, durable entries, restart replay, and proxy registration |
| Ollama routing | Complete | Broker relay and backend scheduler drive proxy routing |
| LM Studio routing | Complete | Parallel broker relay and scheduler path |
| Local engine lifecycle | Complete | Install, start, stop, uninstall, update, and port configuration |
Expand Down Expand Up @@ -419,7 +419,6 @@ provide an equivalent client-facing contract:
| Responsibility | Location |
| ----------------------------------------------------------- | ------------------------------------------------ |
| Poll node telemetry over `/v1/node-info` | `node-info-poller.ts` |
| Persist and replay manual node entries | `manual-nodes-store.ts`, `modular-supervisor.ts` |
| Bridge the local node into engine proxies | `modular-supervisor.ts` |
| Present optimistic engine transition state | `pending-actions.store.ts`, bridge state |
| Serve the model hub (Ollama committed list, LM Studio live) | `src/electron/model-hub/` |
Expand Down
19 changes: 4 additions & 15 deletions desktop/src/electron/service-bridge/empty-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import type { ProxyEngine } from './modular-state'
import type { JsonObject, JsonValue } from './json-rpc-subprocess'
import { emptyInvite, parseClusterNodes, parseInvite, parseNodeIdentity } from './cluster-json'
import { removeManualNodeEntry, resolveManualNodeKey } from './manual-nodes-store'

type BridgeHandler<C extends WsInvokeChannel> = (
payload?: WsInvokeRequest<C>
Expand Down Expand Up @@ -908,20 +907,10 @@ async function handleNodeRemoveMember(
const selfId = state.getSelfId()
const isSelfLeave = selfId !== null && payload.nodeId === selfId

// `payload.nodeId` is the node's UUID, but the manual-nodes store and the
// broker's `node/remove` relay key a manual entry by the name it was added
// with — never the UUID. Map the UUID back through the node's reachable
// addresses to the persisted entry so it is actually pruned (and does not
// reappear on the next replay). If it is not a manual node (no address match)
// fall back to the display hostname, then the raw id; both are harmless
// no-ops for a purely-discovered peer. The broker supervises
// nvpair-manual-nodes and relays `node/*` verbatim.
const manualKey =
resolveManualNodeKey(state.getNodeAddresses(payload.nodeId)) ||
state.getNodeHostname(payload.nodeId) ||
payload.nodeId
removeManualNodeEntry(manualKey)
supervisor.callProcess('broker', 'node/remove', { id: manualKey }).catch(() => {})
// The broker owns manual-node persistence and resolves a stable node UUID
// back to the manual alias expected by nvpair-manual-nodes. For a purely
// discovered peer this remains a harmless no-op.
supervisor.callProcess('broker', 'node/remove', { id: payload.nodeId }).catch(() => {})

if (isSelfLeave) {
// Self-departure is `cluster:leave`, not `nodes:remove` (the cluster-manager
Expand Down
96 changes: 0 additions & 96 deletions desktop/src/electron/service-bridge/manual-nodes-store.ts

This file was deleted.

25 changes: 0 additions & 25 deletions desktop/src/electron/service-bridge/modular-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,31 +1016,6 @@ class ModularBridgeState {
return this.selfId
}

/**
* The display hostname for a node key (UUID), or '' when unknown. Used as a
* last-resort manual-removal key when address matching finds no persisted
* entry (see {@link resolveManualNodeKey}).
*/
getNodeHostname(nodeId: string): string {
return this.nodes.get(nodeId)?.name ?? ''
}

/**
* Every address a node key (UUID) is known to be reachable at — its
* canonical `reachableAddress`, its discovered addresses, and its `host`.
* Used to map a UUID back to the manual-nodes store entry (keyed by the
* user-entered address) when removing a manual node. Empty for an unknown id.
*/
getNodeAddresses(nodeId: string): string[] {
const node = this.nodes.get(nodeId)
if (!node) return []
const out = new Set<string>()
if (node.reachableAddress) out.add(node.reachableAddress)
for (const address of nodeAddresses(node)) out.add(address)
if (node.host) out.add(node.host)
return Array.from(out)
}

/** The authoritative live inbound invites awaiting a PIN, oldest first. */
getPendingInvites(): Invite[] {
return Array.from(this.pendingInvites.values()).sort((a, b) => a.createdAt - b.createdAt)
Expand Down
25 changes: 0 additions & 25 deletions desktop/src/electron/service-bridge/modular-supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
isModularLogLevel,
type ModularLogLevel
} from '@/shared/constants/modular-runtime'
import { listManualNodeEntries } from './manual-nodes-store'
import {
MODULAR_RUNTIME_BINARIES,
modularBinaryFileName
Expand Down Expand Up @@ -841,29 +840,6 @@ class ModularSupervisor {
return [...args, ...this.logLevelArgs()]
}

/**
* Re-add persisted manual nodes through the broker's `node/add` relay. The
* broker's `nvpair-manual-nodes` loses its in-memory entries on restart (N86),
* so PAIR UI owns the durable list (`manual-nodes.json`) and replays it once
* the broker is ready.
*/
private async replayManualNodes(): Promise<void> {
const entries = listManualNodeEntries()
for (const entry of entries) {
try {
await this.callProcess('broker', 'node/add', {
address: entry.address,
name: entry.name
})
} catch (err) {
log.warn({
sublevel: 'manual-nodes',
message: `Failed to replay manual node ${entry.address}: ${getErrorString(err)}`
})
}
}
}

private async onBrokerReady(): Promise<void> {
const subscribe = async (method: string, label: string): Promise<void> => {
try {
Expand All @@ -885,7 +861,6 @@ class ModularSupervisor {

await this.syncClusterIdentityToManager()
await this.resolveSelfId()
await this.replayManualNodes()
await this.hydrateEngineManager()
this.startInstalledEnginesOnFirstOpen()
await this.seedClusterPeerIds()
Expand Down
4 changes: 0 additions & 4 deletions desktop/tests/modular/modular-supervisor-readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ vi.mock('@/electron/service-bridge/broadcaster', () => ({
emitBridgePush: mocks.emitBridgePush
}))

vi.mock('@/electron/service-bridge/manual-nodes-store', () => ({
listManualNodeEntries: () => []
}))

vi.mock('@/electron/service-bridge/node-info-poller', () => ({
startNodeInfoPoller: vi.fn(),
stopNodeInfoPoller: vi.fn()
Expand Down
4 changes: 2 additions & 2 deletions services/nvpair-ui-broker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ Any `settings/*` request is forwarded to `nvpair-node-settings` and its response

#### `node/add` / `node/remove` / `nodes/list` (manual nodes)

Relayed to `nvpair-manual-nodes`. `node/add` (`{ address, name?, tls_port?, mtls? }`) registers a user-added node and probes it; `node/remove` (`{ id }`) drops it; `nodes/list` returns the tracked manual nodes. Manually added nodes also surface in the shared `discovery:get-nodes` / `discovery:nodes-changed` snapshot — the broker merges `nvpair-manual-nodes`' `node/discovered|updated|removed` into the same store the scanner feeds. A `nvpair-manual-nodes` restart loses the in-memory entries because neither that worker nor the broker persists an authoritative copy, so clients must re-add manual nodes after a restart. Error `-32000 "manual-nodes not available"` when no manual-nodes worker is supervised.
Relayed to `nvpair-manual-nodes`. `node/add` (`{ address, name?, tls_port?, mtls? }`) registers a user-added node and probes it; `node/remove` (`{ id }`) drops it; `nodes/list` returns the tracked manual nodes. Manually added nodes also surface in the shared `discovery:get-nodes` / `discovery:nodes-changed` snapshot — the broker merges `nvpair-manual-nodes`' `node/discovered|updated|removed` into the same store the scanner feeds. The broker persists successful `node/add` operations to `configs/manual-nodes.json` in the per-user data directory and replays them into a replacement `nvpair-manual-nodes` worker after startup. Successful `node/remove` operations update the same durable list. Error `-32000 "manual-nodes not available"` when no manual-nodes worker is supervised.

**Manual → proxy bridge.** When the broker supervises both `nvpair-manual-nodes` and a proxy, it also bridges a manual node whose engine is reachable into that proxy via `node/add-manual` (host/port from the node's per-engine status), so inference can route to it through `proxy:node/select` / `lmstudio-proxy:node/select` just like a relay-discovered node. This is per-engine: a node whose `ollama_*` status is up is bridged into `ollama-proxy` (host/port from `ollama_port`), and one whose `lmstudio_*` status is up into `lmstudio-proxy` (from `lmstudio_port`) — a node running both is bridged into both. Manual nodes are by definition the ones that never appear via the daemon's `_nvpair-node` discovery, so this explicit add is what makes them routable. The bridge tracks reachability: an engine that goes down (or a node that is removed, or whose prober crashes) is pulled back out with `node/remove-manual`. A proxy that isn't supervised → that leg is a no-op; manual nodes still appear in the discovery snapshot as before.

Expand Down Expand Up @@ -555,7 +555,7 @@ Attach to a pre-existing endpoint:

- **Engine-advertise control surface.** Engine registration is auto-driven only: the broker tracks local ollama / LM Studio on their fixed coordinates and registers `ol` / `lm` with the daemon while up. There's no manual-advertise RPC (custom service, port, name, or TXT), and no way to advertise anything other than the detected engines.
- **node-info control surface.** node-info is spawned and torn down with the broker, and the broker pushes it only two things over stdin: the log level, and this node's cluster principal (`nodeinfo:set-cluster-identity`, sent on spawn and on every membership or pin-set change, because node-info holds no cluster dir and so cannot read membership itself). Otherwise it's hands-off: the broker registers its port with the daemon (which enriches over plain HTTP) but doesn't pass through TLS material (`--cert` / `--key` / `--client-ca`) or a custom `--port`, and exposes no RPC to query or reconfigure it. It runs with its own defaults plus those two pushes.
- **Manual-node persistence across restarts.** `nvpair-manual-nodes` keeps its entries only in memory and the broker holds no authoritative copy, so a manual-nodes crash-and-restart loses the user's manual nodes (the broker evicts the orphaned entries from the snapshot; clients must re-add them).
- **Manual-node persistence across restarts.** `nvpair-manual-nodes` remains an in-memory worker; the broker owns `configs/manual-nodes.json`, clears stale live projections when the worker exits, and replays the durable entries into its replacement.
- **Per-event push semantics.** `discovery:nodes-changed` always carries the full current snapshot, not a delta. For small N this is fine and lets the client treat the payload as authoritative without state reconciliation. `errors:update` is likewise a full snapshot.
- **BYO-TLS node-info discovery.** The daemon enriches peers over plain HTTP per the consolidated transport policy (node-info is plain on the broker path). Reading a node-info served over operator-configured BYO-TLS / mTLS is not supported through the broker; discovery works for the plain-HTTP case (the common one).
- **Per-worker log-level granularity.** `log/set-level` fans the broker's single level out to every running child, but there's no way to set a different level per worker — it's one level for the whole process tree.
Expand Down
Loading