Skip to content

fix: reconcile agent profile on startup when relay publish was missed#921

Merged
wpfleger96 merged 5 commits into
mainfrom
wpfleger/profile-sync-self-healing
Jun 9, 2026
Merged

fix: reconcile agent profile on startup when relay publish was missed#921
wpfleger96 merged 5 commits into
mainfrom
wpfleger/profile-sync-self-healing

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a managed agent is created, sync_managed_agent_profile() publishes the agent's kind:0 profile event to the relay. This can fail silently — the error is captured in profileSyncError but the frontend ignores it. The result: agents run with no kind:0 on the relay, so channel views show no name or avatar for the agent.

Additionally, the avatar resolution path diverged between creation and reconciliation — creation used input.avatar_url with a fallback, while reconciliation re-derived from persona config, potentially overwriting user intent on every restart.

Solution

Add a fire-and-forget profile reconciliation step to start_managed_agent. Every time an agent starts successfully, a background task:

  1. Queries the relay for the agent's existing kind:0 event via query_agent_profile() (using the agent's stored relay URL for endpoint consistency)
  2. Compares the existing display_name and picture against the stored canonical values on ManagedAgentRecord
  3. If missing or stale → re-publishes the profile via sync_managed_agent_profile()
  4. If current → no-op (no redundant publish)
  5. On any error → logs a warning via eprintln!, never blocks agent startup

The avatar URL is resolved once at creation time and persisted on the record (avatar_url field). Reconciliation compares against this stored value — no re-derivation from persona config, no divergence between create and reconcile paths. Pre-existing records without the field deserialize cleanly via #[serde(default)]; for those, the expected avatar falls back to the legacy persona/command derivation so they self-heal without regressing a previously published avatar to nothing.

Changes

  • desktop/src-tauri/src/relay.rs — added query_relay_at() (parameterized relay URL), query_agent_profile(), AgentProfileInfo struct
  • desktop/src-tauri/src/commands/agents.rs — added avatar_url: Option<String> to ManagedAgentRecord, ProfileReconcileData, reconcile_agent_profile(), profile_needs_sync(), and the spawn call in start_managed_agent. resolve_avatar_for_reconcile() is retained only as the fallback for records that predate the avatar_url field.
  • desktop/scripts/check-file-sizes.mjs — file size override for agents.rs
  • Test fixtures updated for new avatar_url field in managed_agents/{nest,runtime,relay_mesh}.rs

@wpfleger96 wpfleger96 requested a review from a team as a code owner June 9, 2026 16:11
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 added 4 commits June 9, 2026 14:23
When a managed agent starts, the initial profile sync at creation time
may have failed silently, leaving the agent with no kind:0 event on the
relay (no name/avatar in channel views).

Add a fire-and-forget background task to start_managed_agent that:
- Queries the relay for the agent's existing kind:0 profile
- Re-publishes if missing or stale (display_name/picture mismatch)
- Derives avatar from persona config or command-based fallback
- Never blocks agent startup — logs warning on failure

Also adds query_agent_profile() helper to relay.rs for querying kind:0
events by pubkey.

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
… mismatch

Two issues from code review:

1. Avatar resolution divergence: reconciliation re-derived the avatar URL
   from persona config, which could differ from what was originally published
   (the create path uses input.avatar_url, not persona.avatar_url). Fix:
   persist the resolved avatar_url on ManagedAgentRecord at creation time
   and compare against the stored value during reconciliation.

2. Relay endpoint mismatch: query_agent_profile used the workspace relay
   override while sync_managed_agent_profile used the agent's stored
   relay_url. Under an active override, the query hit relay A while the
   publish hit relay B. Fix: extract query_relay_at() helper that accepts
   an explicit base URL, and have query_agent_profile target the agent's
   stored relay_url — same endpoint the publish uses.

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
Records created before avatar_url was persisted deserialize with None.
Comparing None against a published picture would re-publish an empty
avatar, regressing it on every start. Resolve the expected avatar from
the stored value first, falling back to the legacy persona/command
derivation only when absent, so old records self-heal without losing
their avatar.

Bumps the agents.rs line-size override to cover the fallback helper;
the file is already tracked for splitting.

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
resolve_avatar_for_reconcile preferred persona.avatar_url for pre-existing
records (avatar_url: None), but the original create path never consulted
persona — it used input.avatar_url with managed_agent_avatar_url fallback.
This caused reconciliation to compute a different expected avatar than what
was published, silently overwriting user intent on every restart.

Replace with reconcile_avatar that derives solely from
managed_agent_avatar_url(agent_command) for legacy records, matching what
was actually published. Stored avatar_url (new records) still wins.

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
@wpfleger96 wpfleger96 force-pushed the wpfleger/profile-sync-self-healing branch from 6223421 to f1af79e Compare June 9, 2026 18:23
Rename path now uses stored avatar_url with command fallback, matching
the create and reconcile paths. Malformed kind:0 content is treated as
missing (triggers re-publish) instead of bailing reconciliation.
@wpfleger96 wpfleger96 merged commit 762a459 into main Jun 9, 2026
15 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/profile-sync-self-healing branch June 9, 2026 19:05
wpfleger96 pushed a commit that referenced this pull request Jun 9, 2026
Legacy ManagedAgentRecords (created before PR #921) have avatar_url: None
because the field was added with #[serde(default)]. Previously, reconciliation
fell back to the generic runtime icon, overwriting custom persona avatars.

Now, on first start after update, reconcile_agent_profile queries the relay
for the agent's existing kind:0 profile and backfills avatar_url from the
picture field. The backfilled value is persisted so migration runs only once.
If no profile exists on the relay, falls back to the command-derived icon.

After backfill, normal reconciliation proceeds uniformly for all records —
no special-casing needed for legacy vs post-PR-921 agents.

Removes the reconcile_avatar helper (migration handles legacy directly).

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
wpfleger96 pushed a commit that referenced this pull request Jun 9, 2026
The startup avatar backfill read the relay's kind:0 picture as the
source of truth, but PR #921's old reconciliation had already
overwritten that picture with the command default — so the backfill
locked in the wrong avatar. Persona-backed agents now resolve their
backfill avatar from PersonaRecord.avatar_url first, falling back to
the relay picture and command icon only when no persona avatar exists.
wpfleger96 pushed a commit that referenced this pull request Jun 9, 2026
Legacy ManagedAgentRecords (created before PR #921) have avatar_url: None
because the field was added with #[serde(default)]. Previously, reconciliation
fell back to the generic runtime icon, overwriting custom persona avatars.

Now, on first start after update, reconcile_agent_profile queries the relay
for the agent's existing kind:0 profile and backfills avatar_url from the
picture field. The backfilled value is persisted so migration runs only once.
If no profile exists on the relay, falls back to the command-derived icon.

After backfill, normal reconciliation proceeds uniformly for all records —
no special-casing needed for legacy vs post-PR-921 agents.

Removes the reconcile_avatar helper (migration handles legacy directly).

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
wpfleger96 pushed a commit that referenced this pull request Jun 9, 2026
The startup avatar backfill read the relay's kind:0 picture as the
source of truth, but PR #921's old reconciliation had already
overwritten that picture with the command default — so the backfill
locked in the wrong avatar. Persona-backed agents now resolve their
backfill avatar from PersonaRecord.avatar_url first, falling back to
the relay picture and command icon only when no persona avatar exists.

Signed-off-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
tlongwell-block pushed a commit that referenced this pull request Jun 10, 2026
* origin/main:
  Fix post-compact handoff context for OpenAI providers (#931)
  chore(release): release version 0.3.15 (#936)
  fix: persona is source of truth at spawn + thread-depth conventions (#930)
  fix: skip avatar reconciliation for legacy agent records (#933)
  feat(desktop): add nest commit identity guidance with human sign-off (#929)
  feat: provider/model selection for personas and runtime-aware env injection (#794)
  fix: reconcile agent profile on startup when relay publish was missed (#921)
  Revamp first-run onboarding (#924)
  Update setup loading screen (#926)
  fix(dm): keep hidden DMs hidden across refetch via relay-signed visibility snapshot (NIP-DV) (#857)
  Maximize desktop window on launch (#925)
  feat: preview features (experiments settings UI) (#888)
  fix(updater): send no-cache header on update check to avoid stale manifest (#922)
  fix(desktop): refresh channel state after unarchive (#923)
  Add channel visibility & ephemeral TTL controls to manage sidebar (#911)
  ci(release): add Intel macOS (x86_64) DMG as a release target (#748)

Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

# Conflicts:
#	desktop/src/features/sidebar/ui/AppSidebar.tsx
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.

1 participant