Skip to content

fix(profile): merge kind:0 writes instead of replacing them - #3294

Open
bosoud wants to merge 1 commit into
block:mainfrom
bosoud:fix/profile-merge-preserve-fields
Open

fix(profile): merge kind:0 writes instead of replacing them#3294
bosoud wants to merge 1 commit into
block:mainfrom
bosoud:fix/profile-merge-preserve-fields

Conversation

@bosoud

@bosoud bosoud commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #2534.

kind:0 is replaceable, so a profile built from a fixed set of keys is not a partial update — it deletes everything it did not build. build_profile constructed content from a 5-key allowlist, so bot, website, banner and lud16 were dropped the first time Buzz touched a profile that had them, silently and with exit 0.

bot is the expensive one: an agent that quietly loses its NIP-24 flag reads as a human account to every client that renders it.

Approach

buzz-sdk gains ProfileFields and merge_profile_content — a merge over the identity's current content that carries unknown keys forward, so a key from a future NIP also survives a Buzz write.

merge_profile_content is plain serde_json with no nostr types in its signature. That is deliberate: it crosses the nostr 0.36/0.37 boundary that forces desktop/src-tauri/src/events.rs to duplicate the SDK builders, so the duplicated profile-content logic is deleted rather than fixed in two places (item 5 of the issue). Desktop keeps only its own thin build_profile_merged wrapper for the 0.37 EventBuilder, and has no full-replacement variant left to call by accident.

The three write paths

CLI set-profile merges into the full fetched object instead of rebuilding five keys. That also removes the display_namecurrent.display_namecurrent.name fallback chain, which promoted a short handle into display_name for identities that had name but no display_name, and stops passing name: None on every write. New flags: --username (the NIP-01 name field), --website, --bot. --name still maps to display_name, so existing usage is unchanged.

Desktop update_profile and the deferred avatar save merge rather than rebuild from per-key fallbacks.

Desktop managed-agent sync now reads the agent's live profile and merges into it. This is the path the issue calls out as worst: it published a two-field event, and it is unattended — reached from agent save, model change, persona edit, persona and team snapshot import, profile reconciliation and managed-agent restore. AgentProfileInfo carries the full content object so the merge has a base. A failed or absent read degrades to an empty base, so a transient query error cannot block the sync.

Semantics

Unchanged for callers: None leaves a field alone; Some(v) sets it. Clearing a field is still not expressible — that needs a distinct "unset" signal and is deliberately left out of this change.

Tests

New in buzz-sdk, including the issue's exact repro (set one field, assert nothing else moved) with an unmodelled some_future_nip_key pinned to prove passthrough. New in desktop/src-tauri/src/relay.rs: the managed-agent sync preserves about/nip05/bot/website while updating name and avatar.

cargo test --workspace --lib and cargo clippy --workspace --all-targets are clean. The desktop Tauri crate type-checks clean (cargo check --tests, no warnings); I could not execute its test binaries locally because sherpa-onnx-sys has no prebuilt static lib for macOS x86_64, so CI is the first place those two desktop tests actually run.

kind:0 is replaceable, so publishing a profile built from a fixed set of
keys is not a partial update — it deletes every field that was not built.
Buzz constructed kind:0 content from a 5-key allowlist, so any identity
whose profile carried `bot`, `website`, `banner` or `lud16` lost them the
first time Buzz touched the profile, silently and with exit 0.

`bot` is the costly one: an agent that quietly loses its NIP-24 flag reads
as a human account to every client that renders it.

Add `ProfileFields` and `merge_profile_content` to buzz-sdk: a merge over
the identity's current content that carries unknown keys forward, so a key
from a future NIP also survives contact with Buzz. The merge is plain
serde_json with no nostr types, so the desktop crate shares it across the
nostr 0.36/0.37 boundary that keeps these builders duplicated — the
duplicated content-building logic in desktop/src-tauri/src/events.rs is
gone rather than fixed twice.

Fix all three write paths:

- CLI `set-profile` merges into the full fetched object. This also drops
  the `display_name` -> `current.display_name` -> `current.name` fallback
  chain, which promoted a short handle into `display_name` for identities
  that had `name` but no `display_name`, and stops hardcoding `name` to
  None on every write. Adds `--username`, `--website` and `--bot`;
  `--name` keeps mapping to `display_name` for compatibility.
- Desktop `update_profile` and the deferred avatar save merge rather than
  rebuild from per-key fallbacks.
- Desktop managed-agent sync reads the agent's live profile and merges into
  it. This path published a two-field event and is unattended — reached
  from agent save, model change, persona edit, persona and team snapshot
  import, profile reconciliation and managed-agent restore — so it erased
  `about`/`nip05`/`bot` with no user action. The read degrades to an empty
  base on failure, so a transient query error cannot block the sync.

Field semantics are unchanged for callers: `None` leaves a field alone.
Clearing a field is still not expressible and is left out deliberately.

Fixes block#2534

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164eVvBNsWPzJZVtkDJAqPx
Signed-off-by: Abdulwahab <bosoud@me.com>
@bosoud
bosoud requested a review from a team as a code owner July 28, 2026 11:49
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.

kind:0 profile republish drops every field outside a 5-key allowlist — silently deletes bot: true, nip05 and website

1 participant