Skip to content

bug: a stored host whose serverId no longer exists is never reconciled or removed #5802

Closed as not planned

Description

@mogul

Why this framing. #637 asked for an identity-model audit and was closed as feature-request intake. This is deliberately not that: no redesign, just a reachable, permanently-broken user state with no in-app recovery. Filing as the bug it is, per CONTRIBUTING.


What's broken

A host in the registry whose daemon has been replaced by one with a different serverId becomes permanently unusable, with no in-app way to fix it. The host stays listed, every connection attempt fails, and the only recovery is clearing browser storage — which is not discoverable and also destroys unrelated state.

Concretely: my registry kept pointing at a srv_… for a daemon that no longer existed anywhere on the machine, while the daemon actually reachable at that same endpoint had a different id. The stale entry survived reloads indefinitely.

Why it never recovers (verified on v0.10.2)

Two paths, and only one heals:

  • Placeholder ids heal. packages/app/src/runtime/host-runtime.ts:1014 — on an id mismatch, if the stored id is a placeholder (isPlaceholderServerId, local:-prefixed, :1427), it calls onReconcileServerId and rekeys everything (reconcileServerId, :1716).

  • Real ids do not. For a non-placeholder stored id the same branch closes the client and throws:

    throw new Error(`Connection resolved to ${serverId}, expected ${this.host.serverId}.`);

    Nothing removes, flags, or reconciles the profile. The next attempt takes the identical path.

Load-time cleanup has the same asymmetry — loadFromStorage (:1583) filters out placeholder-id profiles and re-persists, but a real-looking srv_… that exists nowhere is preserved verbatim, forever:

profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId));

So the gap is narrow and specific: a stored real serverId that no longer resolves has no reconciliation path and no eviction path.

Steps to reproduce

  1. Open the web UI against a daemon at a direct endpoint (e.g. localhost:6767). A profile is stored with that daemon's serverId.
  2. Replace that daemon with a fresh one on the same endpoint, so it generates a new serverId. Any of: a new PASEO_HOME, a fresh keypair, or — the case I hit — recreating the container/VM the daemon runs in while the published host port stays the same.
  3. Reload the web UI.

Expected: the host reconciles to the new serverId (as it already does for placeholder ids), or is clearly reported as a different daemon with an offered action.

Actual: the host remains listed and never connects. The console repeats Connection resolved to <new>, expected <old>. Reloading does not help — the entry is re-read from storage unchanged. The only fix is DevTools ▸ Application ▸ Storage ▸ Clear site data, or deleting @paseo:daemon-registry by hand.

Worth noting "Empty Cache and Hard Reload" does not clear Local Storage, so the remedy a user would naturally try doesn't work — which is much of why this reads as "the app is broken" rather than "stale local state."

Why this is routine, not an edge case

Container and VM workflows hit step 2 as ordinary maintenance: recreating the VM regenerates the daemon identity while the published endpoint stays byte-identical. The client has no way to notice, because the endpoint it stored is the thing that didn't change.

Suggested direction (not a redesign)

Any one of these closes it; no preference:

  • Treat a real-but-unresolvable stored id like a placeholder in the mismatch branch — reconcile to the observed serverId when the connection matches and the old id answers nowhere.
  • Or evict: if a probe resolves a different serverId on a stored connection, drop the stale profile and upsert the observed one. upsertHostConnectionInProfiles (packages/app/src/types/host-connection.ts:234) already matches profiles by serverId or by connection equality, so the merge machinery exists.
  • Or at minimum surface it: show the host as "different daemon at this address" with a one-tap Forget host. Even without automatic reconciliation that removes the dead end.

Environment

  • Paseo 0.10.2 (source-verified; hit on 0.9.1, and all paths cited above are unchanged between the two tags)
  • Web (browser) surface; Chrome, macOS 15 arm64; direct TCP connection, relay disabled

GSA-TTS engineer. Investigation AI-assisted (OpenCode); source claims human-verified against v0.10.2.

Activity

  1. paseo-bot commented on Sep 30, 2026

    @paseo-bot
    Contributor

    Thanks for writing this up. I'm closing it because issues here are for bugs, and Paseo refusing a daemon whose server ID differs from the one it stored is deliberate: only placeholder IDs are reconciled. Having it adopt a recreated daemon at the same address is a workflow change; ideas get read in Discussions, and there's one about recovering a host after its daemon is recreated at #4132.

    The stale entry can be removed without clearing site data: open that host in Settings and use Remove host, then add the endpoint again to register the new daemon.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    triagedReviewed by maintainer triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions