refactor(agents): replace avatar-clear boolean with AvatarState tri-state enum#1232
Closed
tellaho wants to merge 2 commits into
Closed
refactor(agents): replace avatar-clear boolean with AvatarState tri-state enum#1232tellaho wants to merge 2 commits into
tellaho wants to merge 2 commits into
Conversation
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>
This was referenced Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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>overloadedNoneto 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,mainhad no avatar-clear path at all —UpdateManagedAgentRequestexposed noavatar_urlfield, so a clear couldn't even be expressed.Solution: Replace
Option<String>with a singleAvatarStatetri-state enum onManagedAgentRecord:Set(url)— explicit url, use as-is, never backfillCleared— user deliberately cleared, stays empty, never backfillUnmigrated— legacy/unknown, eligible for backfill once, then becomesSet/ClearedThe three states are mutually exclusive and type-enforced, so every read site must handle all three and no
Noneis left to overload. Custom serde keeps this zero data migration: legacy bare-string maps toSet,null/absent maps toUnmigrated(preserving today's backfill-once semantics byte-stably), andClearedserializes to a self-describing{"cleared":true}sentinel that no legacy record can ever produce.Clearedis reachable only through an explicit user clear via the update path (Option<Option<String>>, matching the existingmodelfield 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. Theapp-avatar:<id>symbolic-ref scheme is independent and untouched.File changes
desktop/src-tauri/src/managed_agents/types.rs
Defines the
AvatarStateenum + 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
AvatarStatematch (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
ManagedAgentRecordfixtures/constructors fromavatar_url: NonetoAvatarState::Unmigrated.desktop/scripts/check-file-sizes.mjs
Narrowly-scoped types.rs size override for the tri-state code + tests.
Reproduction Steps
tho/agent-avatar-clear-tristateand runjust desktop-tauri-test— 584 lib tests pass (6 new).AvatarStateinmanaged_agents/types.rsand the three-arm match incommands/agents.rs.types.rsprove all four on-disk cases map correctly and theClearedto{"cleared":true}toClearedsentinel holds.Validation
584 lib tests pass (0 fail), clippy
-D warningsclean, fmt no churn. Pre-commit/pre-push hooks green except the knowndart-missing mobile hook (no Dart touched).