Skip to content

refactor(agents): replace avatar-clear boolean with AvatarState tri-state enum#1232

Closed
tellaho wants to merge 2 commits into
mainfrom
tho/agent-avatar-clear-tristate
Closed

refactor(agents): replace avatar-clear boolean with AvatarState tri-state enum#1232
tellaho wants to merge 2 commits into
mainfrom
tho/agent-avatar-clear-tristate

Conversation

@tellaho

@tellaho tellaho commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Overview

Category: improvement
User Impact: A deliberately cleared agent avatar now stays cleared instead of being silently resurrected on the next sync.

Problem: ManagedAgentRecord.avatar_url: Option<String> overloaded None to mean two contradictory things — "legacy record, backfill a default" and "user deliberately cleared it." That ambiguity is the bug: a user-cleared avatar gets backfilled (resurrected) on the next reconcile. On top of that, main had no avatar-clear path at all — UpdateManagedAgentRequest exposed no avatar_url field, so a clear couldn't even be expressed.

Solution: Replace Option<String> with a single AvatarState tri-state enum on ManagedAgentRecord:

  • Set(url) — explicit url, use as-is, never backfill
  • Cleared — user deliberately cleared, stays empty, never backfill
  • Unmigrated — legacy/unknown, eligible for backfill once, then becomes Set/Cleared

The three states are mutually exclusive and type-enforced, so every read site must handle all three and no None is left to overload. Custom serde keeps this zero data migration: legacy bare-string maps to Set, null/absent maps to Unmigrated (preserving today's backfill-once semantics byte-stably), and Cleared serializes to a self-describing {"cleared":true} sentinel that no legacy record can ever produce. Cleared is reachable only through an explicit user clear via the update path (Option<Option<String>>, matching the existing model field convention) — an invariant covered by tests. Avatar updates now sync to relay immediately, consistent with name changes.

Scope note — backend-only. This is the avatar-clear state model plus the backend clear plumbing. Wiring the TS UI to actually send a clear (UpdateManagedAgentInput.avatarUrl) is a separate UX follow-up and is intentionally not in this PR. The app-avatar:<id> symbolic-ref scheme is independent and untouched.

File changes

desktop/src-tauri/src/managed_agents/types.rs
Defines the AvatarState enum + custom serde (string to Set, null/absent to Unmigrated, {"cleared":true} to/from Cleared); swaps the record field; adds the direct round-trip + sentinel tests. File-size override bumped for the added tests.

desktop/src-tauri/src/commands/agents.rs
Collapses the reconcile path into a three-arm AvatarState match (Set use-as-is / Cleared no-backfill / Unmigrated backfill-then-persist-as-Set).

desktop/src-tauri/src/commands/agent_models.rs
Adds the Option<Option<String>> avatar update field (absent = don't touch, null = Cleared, Some(url) = Set).

desktop/src-tauri/src/commands/agents_tests.rs
Adds the creation invariant test: no creation/constructor path can yield Cleared.

desktop/src-tauri/src/managed_agents/{nest,relay_mesh,runtime/tests,team_repair}.rs
Updates the non-user-facing ManagedAgentRecord fixtures/constructors from avatar_url: None to AvatarState::Unmigrated.

desktop/scripts/check-file-sizes.mjs
Narrowly-scoped types.rs size override for the tri-state code + tests.

Reproduction Steps

  1. Check out tho/agent-avatar-clear-tristate and run just desktop-tauri-test — 584 lib tests pass (6 new).
  2. Inspect AvatarState in managed_agents/types.rs and the three-arm match in commands/agents.rs.
  3. Confirm the round-trip tests in types.rs prove all four on-disk cases map correctly and the Cleared to {"cleared":true} to Cleared sentinel holds.

Validation

584 lib tests pass (0 fail), clippy -D warnings clean, fmt no churn. Pre-commit/pre-push hooks green except the known dart-missing mobile hook (no Dart touched).

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 2 commits June 23, 2026 18:32
Agent avatar_url overloaded `None` to mean two contradictory things:
"legacy record, backfill a default" and "user deliberately cleared it."
That ambiguity was the bug — a user-cleared avatar got resurrected from a
default on the next reconcile.

Replace the Option<String> + avatar_url_cleared bool combo with a single
AvatarState tri-state enum (Set(url) / Cleared / Unmigrated) on
ManagedAgentRecord. The three states are mutually exclusive and
type-enforced, so there is no `None` left to overload and every read site
must handle all three. Custom serde impls keep pre-existing records
readable with no data migration: a JSON string -> Set, null/absent ->
Unmigrated (legacy backfill-once preserved, round-trips byte-stable),
and the sentinel { "cleared": true } -> Cleared (forward-only; never
written by legacy records). Avatar updates now sync to the relay
immediately, consistent with name changes — a Cleared publishes a kind:0
with no picture.

Scope: avatar-clear state model only. The app-avatar:<id> symbolic-ref
scheme stands on its own and is untouched.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Adds focused tests proving the AvatarState back-compat guarantee directly,
the load-bearing safety net flagged in review:

- legacy bare string -> Set(url)
- JSON null -> Unmigrated
- absent field (via #[serde(default)]) -> Unmigrated
- Cleared -> serialize to {"cleared":true} -> deserialize -> Cleared
  (forward-only sentinel round-trip; the case guarding a future refactor
  from silently breaking the cleared-avatar marker)
- Set/Unmigrated round-trip

Also adds the creation invariant: resolve_created_avatar_url with no input,
no persona, and an unresolvable command yields Unmigrated, never Cleared --
Cleared must be reachable only via an explicit user clear through the update
path. Bumps the types.rs file-size override 1100 -> 1180 for the added tests.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho changed the title Replace avatar-clear boolean with AvatarState tri-state enum refactor(agents): replace avatar-clear boolean with AvatarState tri-state enum Jun 24, 2026
@tellaho tellaho closed this Jun 24, 2026
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