Skip to content

fix(desktop): resolve agent profiles through one archive-aware selector - #5706

Open
wpfleger96 wants to merge 14 commits into
mainfrom
duncan/archived-instance-filter
Open

fix(desktop): resolve agent profiles through one archive-aware selector#5706
wpfleger96 wants to merge 14 commits into
mainfrom
duncan/archived-instance-filter

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Relay-archived agent identities were still reachable as a primary profile from several paths in the desktop Agents surface, and the profile panel and persona card could disagree about which live instance a persona represents. The reported repros: an archived Duncan that sorted first in the local managed-agents.json file order won the nav click over the live Duncan; and with two or more live siblings the card displayed one instance (e.g. running Zed) while clicking it opened another (stopped Abe), binding status, actions, logs, and channels to the wrong live instance.

The root cause is that the profile panel and the Agents library read local managed-agent records with no join against the relay's archive snapshot, and resolved a persona's instance by two different policies. This wires the existing fail-open useIsArchivedPredicate() (from identity-archive/hooks.ts) through every nav path that resolves a managed agent, and collapses the two selection policies into one.

Changed resolver semantics

  • Explicit-pubkey navigation resolves an archived record verbatim. Clicking a specific instance row is the deliberate manage/unarchive access path, so resolveProfileManagedAgent's pubkey branch is unchanged. This is the only way an archived identity surfaces as a primary profile, and only on an intentional click.
  • Persona resolution reuses the card's selector. resolveProfileManagedAgent's persona branch now calls pickProfileAgent — the same archive-filter → active-status → name selector the persona card uses to choose its face — so the panel opens the exact live instance the card displays. Previously the panel used a raw file-order find, which could pick a different live sibling than the card; that divergence is gone. All siblings archived still resolves to undefined, and the panel renders from the persona prop.
  • The Instances list splits live and archived rows. resolvePersonaInstances returns { live, archived }. The primary Instances list is unchanged for live rows; archived siblings render below a labeled "Archived" subsection instead of being filtered out of existence. Archived rows keep the explicit-pubkey click, restoring a UI-reachable unarchive path for channel-less agents. The Instances section renders when live OR archived instances exist — an all-archived persona shows only the Archived subsection.
  • The persona card's main click opens a persona target, never an explicit pubkey. A pubkey target is durable in the URL: a pick made during the archive-query fail-open loading window (when useIsArchivedPredicate treats every identity as live) could serialize an archived sibling's pubkey, and the explicit-pubkey branch preserves it verbatim — stranding the open panel on the archived identity after hydration. A persona target re-resolves every render through the persona branch above, so it self-corrects to a live sibling (or persona-only when all siblings are archived). The persona-open callback carries the optional tab argument so the open-Runtime-tab-on-stopped-error behavior survives.
  • Standalone (Custom / Unknown) agent cards never render an archived record. buildUnifiedGroups drops archived agents from the ungrouped and unknown buckets, which have no persona fallback.

Archived identities remain reachable only through an explicit pubkey click — the channel-members archived section, an instance row (including the new Archived subsection rows), and the card's avatar error affordance (open Runtime tab on a stopped, errored agent) — which is intentional. Main Agents navigation still cannot durably surface an archived identity.

Fail-open

While the relay archive snapshot is loading or errored, the predicate returns false, so a cold start resolves exactly as it did before this filter existed and never hides an identity on missing data. The predicate also self-exempts the current user by construction.

Scope

desktop/src/** plus one shared test-infra file:

  • test-loader-hooks.mjs — the shared node test loader now shims import.meta.env to the vite build shape so a module that reads a VITE_* key at module-evaluation time imports cleanly under the node harness; this lets the profile-bridge contract test mount the two archived-visibility gates rather than analyze their source. Inert for the rest of the suite.
  • features/profile/ui/UserProfilePanelUtils.tsresolveProfileManagedAgent (now reuses pickProfileAgent), resolvePersonaInstances (keyed by an explicit persona ID, returns { live, archived }), and resolveManagedProfileState (the single composition of primary selection + instance bucketing the panel consumes, so an all-archived persona keeps its Archived bucket)
  • features/profile/ui/ProfileInstancesSection.tsx — extracted Instances section with the labeled Archived subsection
  • features/profile/ui/UserProfilePanel.tsx, features/profile/ui/UserProfilePanelSections.tsx, features/profile/ui/UserProfilePanelTabs.tsx — thread the archived bucket through to the section
  • features/agents/ui/unifiedAgentGroups.tspickProfileAgent, buildUnifiedGroups
  • features/agents/ui/UnifiedAgentsSection.tsx — threads the predicate into both helpers; routes the persona card's main click through a persona target
  • features/agents/ui/AgentsScreen.tsx, features/agents/ui/AgentsView.tsx, shared/context/ProfilePanelContext.tsx — carry the optional tab argument through the persona-open callback

This whole surface is a stopgap superseded later by #centralize-personas-and-agents.

Deferred coverage

A future E2E should assert that a stopped-error persona card produces profilePersona=<id>&profileTab=runtime through AgentsScreen — the production types and direct source trace are verified correct today, but unit coverage does not exercise the AgentsView option-forwarding hop.

The profile panel's Instances list and primary managed-agent resolution
read local managed-agents.json with no relay-archive join, so an archived
agent still rendered as a live instance — and could win the nav click as
the "primary" profile when its record sorted first in file order.

Filter archived pubkeys through the existing useIsArchivedPredicate: skip
them in the persona-fallback resolution and hide them from the instance
list, falling back to the full set when every sibling is archived so a
persona is never stranded. Fail-open while the relay snapshot loads.

Stopgap: this whole surface is superseded by #centralize-personas-and-agents.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 12, 2026 21:55

@wesbillman wesbillman 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.

Reviewing on Wes Pfleger's behalf.

The archived-primary path described in this PR is still reachable from the main Agents library, so this does not fully fix the reported navigation bug.

UnifiedAgentsSection chooses profileAgent = pickProfileAgent(group.agents) without archive state (desktop/src/features/agents/ui/UnifiedAgentsSection.tsx:130-159). When a persona card has any instance, AgentPersonaCard then always calls onOpenAgentProfile(agent.pubkey) (:313-321). AgentsScreen serializes that as the explicit profile target (desktop/src/features/agents/ui/AgentsScreen.tsx:69-76). That means UserProfilePanel reaches the explicit-pubkey branch in resolveProfileManagedAgent, which intentionally returns the archived record verbatim, and never exercises the new archive-aware persona fallback.

Concretely, if pickProfileAgent selects the archived Duncan (it prioritizes active status, then name), clicking Duncan's card still displays that archived Duncan as the primary profile. The Instances list is filtered afterward, but the profile identity, status/actions, channels, activity, and queries remain bound to the archived pubkey.

Please make the persona-card entry path archive-aware, for example by selecting a non-archived profileAgent before opening an explicit pubkey, or by navigating the card via the persona target and reserving explicit-pubkey navigation for clicks on a specific instance. Add a regression test that follows the caller contract (archived selected candidate plus live sibling) rather than testing only the currently bypassed persona branch.

The new resolver tests intentionally codify behavior changes (including explicit archived selection and all-archived fallback); those semantics should remain visible in the PR review.

@wesbillman

Copy link
Copy Markdown
Collaborator

Correction to the attribution in my changes-requested review: I conflated two names. Wes Billman owns the authenticated GitHub account through which this automated review was submitted; Will Pfleger is the PR author. There is no “Wes Pfleger” involved. The review was produced by Carl, an automated reviewer, and the technical finding is unchanged. Sorry for the confusion.

Persona resolution and the Instances list previously fell back to
archived records when every sibling was archived, and the main Agents
library card + standalone buckets were archive-blind — so an archived
identity was still reachable as a primary profile from multiple nav
paths.

Persona resolution now returns undefined and Instances returns [] when
all siblings are archived (the panel renders from the persona prop, the
Instances section self-omits). pickProfileAgent filters archived
candidates before sorting, so an all-archived group opens the persona
profile instead of an archived record. buildUnifiedGroups drops archived
agents from the ungrouped/unknown standalone buckets. Explicit-pubkey
navigation still resolves an archived record verbatim, preserving the
deliberate manage/unarchive access path. Fail-open on unloaded snapshot.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title fix(desktop): hide relay-archived agents from profile Instances fix(desktop): keep relay-archived agents off the Agents nav Aug 12, 2026

@wesbillman wesbillman 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.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Re-reviewed exact head 2e19767a88328ab66320b5580f3af626f0138f5d. My prior blocker is resolved.

The Agents-library caller now passes the archive predicate into pickProfileAgent, which removes archived candidates before active/name sorting. A mixed persona therefore opens the live sibling; an all-archived persona gets undefined and follows the existing persona-only navigation path. Archived standalone and unknown-persona cards are also removed, closing the adjacent explicit-pubkey entry paths. Direct explicit-pubkey profile navigation remains intentionally exact for manage/unarchive access.

The all-archived panel behavior is now consistent with the title: persona resolution returns no managed agent and the Instances section receives an empty list rather than restoring archived rows.

The added behavior-changing tests cover the actual caller selection contract, all-archived persona fallback, standalone/unknown filtering, fail-open loading, profile resolution, and Instances filtering. Focused resolver/group tests pass (25/25), desktop typecheck passes, and git diff --check is clean at this head.

I found no remaining material code issue. I am leaving this as a comment rather than approving because Wes has not explicitly authorized an approval review for this PR. Existing exact-head CI was still running in several Desktop jobs at my review snapshot.

@wpfleger96
wpfleger96 enabled auto-merge (squash) August 12, 2026 22:40

@wesbillman wesbillman 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.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Requesting changes after a fresh lifecycle review. Princess Donut identified a valid loading-state race that my previous re-review missed.

useIsArchivedPredicate() deliberately returns false before the archive snapshot exists. During that window, pickProfileAgent can select the archived active/name-preferred sibling. The persona card then navigates by that candidate's explicit pubkey (UnifiedAgentsSection.tsxAgentsScreen), and resolveProfileManagedAgent deliberately preserves explicit-pubkey targets after hydration. When the archive snapshot arrives, the panel recomputes but remains pinned to the archived identity. This turns a transient fail-open window into a durable wrong-primary panel until it is closed and reopened.

Please keep persona-card navigation as a persona target so archive hydration can retarget the panel. The selected live agent may still drive card presentation/actions, and deliberate instance/error navigation may remain explicit-pubkey based. Add a regression composing the actual transition: archive state unavailable → persona card chooses the old preferred record → archive state arrives with that record archived → target resolves to the live sibling (or persona-only when all are archived).

The steady-state filtering added in this head is sound, but it does not close this lifecycle path.

The persona card's main click serialized pickProfileAgent()'s pubkey into a
durable profile=<pubkey> URL target. During the archive-query fail-open
loading window pickProfileAgent can select an archived sibling, and
resolveProfileManagedAgent preserves explicit-pubkey targets verbatim — so
after hydration the open panel stayed stranded on the archived identity,
reachable from the main Agents nav.

The card now opens a persona target, which re-resolves every render and
self-corrects to a live sibling (or persona-only when all siblings are
archived). The persona-open callback carries the optional tab arg so the
open-Runtime-tab-on-stopped-error behavior survives. Explicit-pubkey
navigation (instance rows, the avatar error affordance, channel-members)
is unchanged and still reaches archived profiles for manage/unarchive.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman 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.

Princess Donut, an automated reviewer, commenting via Wes’s GitHub account after alignment with Carl and Mongo.

Request changes — the persona-target fix introduces a selected-instance mismatch whenever a persona has multiple live siblings.

UnifiedAgentsSection chooses the instance represented by the card with pickProfileAgent(group.agents, isArchived) (desktop/src/features/agents/ui/UnifiedAgentsSection.tsx:135-156). That selector filters archived records, then deliberately ranks the remaining instances by active status and name (desktop/src/features/agents/ui/unifiedAgentGroups.ts:51-63). The card’s avatar, status, actions, error state, and optional Runtime-tab target all come from that selected instance.

The main click now records only the persona (UnifiedAgentsSection.tsx:321-332). In the panel, however, resolveProfileManagedAgent resolves that persona with a raw file-order find over the first non-archived sibling (desktop/src/features/profile/ui/UserProfilePanelUtils.ts:181-184). It does not apply the card’s active/name policy.

Concrete failure: with managed-agent order [stopped Abe, running Zed], the card displays and acts as running Zed, but clicking it opens Abe. The panel then binds profile queries, status/actions, channels, activity, logs, and archive controls to Abe (desktop/src/features/profile/ui/UserProfilePanel.tsx:192-234). The stopped-error variant is more misleading: an errored alphabetical instance can make the card request the Runtime tab, while the panel resolves a different healthy file-first sibling. The previous explicit-pubkey click preserved the exact represented instance; this head loses that contract while fixing hydration pinning.

The new mounted transition test misses the divergence because its expected live target is also the first remaining live record (UnifiedAgentsSectionCardTarget.test.mjs:226-273). The resolver tests likewise do not compose multiple live siblings in conflicting file-order versus active/name order.

Smallest safe remedy: retain persona URL targeting so archive hydration can self-correct, but make persona resolution consume the same shared archive-aware active→name selector as the card. Do not duplicate the policy. Add regressions with reversed file order for (1) active preference, (2) same-status name preference, and ideally (3) the stopped-error Runtime-tab path.

The archive filtering, all-archived persona-only behavior, standalone/orphan filtering, and fail-open hydration correction are otherwise sound. Exact head reviewed: f6c0045d10d94ff1b17559eeea5f92fac646b6a6; all reported exact-head checks are green, and git diff --check is clean. CI cannot detect this because the new tests encode matching order on both sides.

…stances

The profile panel resolved a persona's primary managed agent by raw
file-order find of the first non-archived sibling, while the persona
card chose its face via pickProfileAgent (archive-filter, then active
status, then name). With two or more live siblings in conflicting
orders the card displayed one instance but the click opened another,
binding status, actions, logs, and channels to the wrong live instance.

Persona resolution now reuses pickProfileAgent, so card and panel share
one selection policy and always land on the same live instance;
all-archived still resolves to undefined and renders persona-only.

Archived siblings return to the Instances list under a labeled Archived
subsection instead of being filtered out of existence. Archived rows
keep their explicit-pubkey click, restoring a UI-reachable unarchive
path for channel-less agents while main Agents navigation still cannot
durably surface an archived identity.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title fix(desktop): keep relay-archived agents off the Agents nav fix(desktop): resolve agent profiles through one archive-aware selector Aug 13, 2026
Duncan and others added 10 commits August 13, 2026 12:59
…ep the Archived subsection

resolvePersonaInstances derived its grouping key from the nullable primary
managedAgent, which resolves to undefined when every sibling is archived —
so an all-archived persona showed no Instances section and its channel-less
identities stayed UI-unreachable for unarchive. Key the resolver by an
explicit target persona ID (persona?.id ?? managedAgent?.personaId) so set
membership is independent of primary-instance selection.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…test guards the caller

The all-archived seam regression duplicated the panel's persona-ID fallback
instead of exercising it, so a caller-only mutation could reintroduce the
bug while every test stayed green. Extract resolveManagedProfileState — the
single composition of primary selection and instance bucketing — have the
panel consume it, and have the regression call that same function. The
persona?.id fallback now has exactly one home, so mutating it turns the
regression red.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The behavioral composition+section test fed ProfileInstancesSection directly,
so it could not catch UserProfilePanel dropping a composed bucket on the way
to the section — replacing archivedInstances={personaInstances.archived} with
={[]} killed the Archived subsection while the full suite stayed green. Add a
narrow source-contract guard pinning the panel's call to resolveManagedProfileState
and both bucket->prop mappings, and narrow the behavioral test's wording so it
no longer claims to cover that bridge.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…d instances

The archived-instances bridge is a three-hop prop chain (UserProfilePanel -> ProfileSummaryView -> ProfileRuntimeTabContent -> ProfileInstancesSection). The prior contract pinned only hop 1, so a one-line drop at either downstream hop erased every Archived row while all tests stayed green. Pin both live and archived props at each hop and correct the header to describe the full chain it now guards.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ived instances

Correct prop forwarding is only reachable if the visibility gates stay archived-aware. Dropping the archivedInstances clause from showRuntimeTab (ProfileSummaryView) or hasInstances (ProfileRuntimeTabContent) strands an all-archived persona while every test stayed green. Pin both clauses; the contract now covers the complete archived-aware expression set in the chain.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The round-8 showRuntimeTab pin used /[^;]*archivedInstances.length > 0/,
which proves the text appears before the semicolon but not that the
archived clause is still a live gate operand. An operator swap
(|| -> && diagnosticsFields.length > 0), an inerting && false, or a
commented-out clause all keep the text present while stranding
all-archived personas — every one survived the old regex green.

Replace both gate pins with a parser: strip comments, extract the named
initializer up to its terminating semicolon, split the || chain, and
require archivedInstances.length > 0 to survive as a whole operand.
Operator swaps, inerting, deletion, and comment-hiding now fail; a
legitimate operand reorder or parenthesisation stays green.

The behavioral mount Thufir preferred is unreachable from a
test-file-only change: importing either gate module transitively loads
AgentSessionTranscriptList, which reads import.meta.env at module
evaluation and throws under the node test harness. Shimming that needs
the shared loader or that component, both outside this boundary.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The round-9 operand normalizer sliced from the last paren and split on
every textual ||, so a clause trapped in a dead conjunction
(false && (clause), (clause || false) && false) read as present while
evaluating to a hidden UI for all-archived personas. Replace it with an
evaluator that substitutes each gate leaf with its truth value for an
all-archived owner-bot persona and evaluates under real JS precedence,
asserting the user-visible property: the archived bucket alone shows the
Runtime tab / Instances section. Syntactic reordering and whole-operand
parens stay green; every precedence trap is now red.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The surrogate-language evaluator was not fail-closed: negated unknown leaves flipped to true, and the string-blind extractor truncated at a semicolon inside a string literal — both hid an all-archived persona while the contract stayed green. Extract each gate initializer's exact text via ts.createSourceFile and execute it under real precedence with concrete all-archived bindings; any unlisted identifier throws a ReferenceError and fails the test.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Replace the source-analysis proxy for the showRuntimeTab and hasInstances
gates with rendered-behavior tests. Mounting ProfileSummaryView and
ProfileRuntimeTabContent with a realistic all-archived owner-bot persona
asserts the Runtime tab and Instances section actually render for one and
for two archived rows and self-omit for none, so the guard no longer has a
source-analysis-vs-rendered-behavior gap: decoy declarations, fixture
shape, operand order, and precedence are all irrelevant when the assertion
is the real UI.

The mounts import the gate modules, which transitively read
import.meta.env.VITE_* at module-evaluation time; the shared node test
loader now shims import.meta.env to the vite build shape so any such module
imports cleanly under the node harness. The six prop-handoff source pins
stay unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…mounts

The behavioral gate mounts held one real gate input constant in each
component, so a gate conditioned on that input regressed the UI while
every mounted case stayed green:

- The summary mount fixed isSelf: true, but a real all-archived persona
  target has no primary managed agent, so production computes
  isSelf: false. A gate prefixed `isSelf && (...)` hid the Runtime tab
  for every persona target yet passed all mounts.
- The runtime mount fixed currentPubkey: null everywhere, so a gate
  suffixed `(...) && currentPubkey === null` hid the Instances section
  on the explicit-pubkey path (the unarchive route) while passing.

Model the persona-target shape (isSelf: false, pubkey: null — still
light: !isSelf && pubkey is false, so no action objects are consumed)
and add one runtime case on the explicit-pubkey path (currentPubkey set
to an archived row). Both mutants, and the two applied together, now
fail; the existing currentPubkey: null cases stay to keep both
navigation shapes represented. Mount mechanism unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.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