fix: unified profile#1126
Conversation
|
Claude finished @cameronvoell's task —— View job PR Review: Unified Profile ArchitectureThis is a well-architected refactor that introduces a canonical profile system. The implementation is solid with strong test coverage and thoughtful design patterns. ✅ StrengthsArchitecture & Design:
Code Quality:
Security:
|
How to use the Graphite Merge QueueAdd the label merge-queue to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
cf617a0 to
56c615a
Compare
4337333 to
f800fdd
Compare
6e23b2f to
d592e9c
Compare
d592e9c to
0934417
Compare
f800fdd to
8be2d61
Compare
0934417 to
7d284bf
Compare
7d284bf to
7444f90
Compare
…ar in MyProfileRepository
…s, and self profile migration
…data (#1144) * fix: restore per-conversation scoping for grant/timezone profile metadata The unified-profile cutover (#1126) rerouted ProfileMetadataWriter through the global DBMyProfile.metadata map: conversation-scoped keys (cloud connection grants, agent timezone) were merged into one global map and broadcast to every conversation on each lazy publish. Granting a connection in one conversation overwrote another conversation's grants, grant details and timezone leaked to every conversation the user touched, and the grant writer's send-failure contract (decline to persist the local grant on a failed publish) was silently voided because the global path performs no network send. Restore the scoping with a new selfConversationMetadata table keyed by (inboxId, conversationId): - ProfileMetadataWriter reads and merges the scoped map for one conversation and publishes it immediately through the new ProfilesRepository.publishMyProfileMetadata(_:toConversation:), which throws on failure so grant callers can decline to persist. - ProfilePublisher merges each conversation's scoped keys over the global map at send time (scoped wins), so a later name or avatar publish can never wipe a conversation's grants at the receiver. The scoped map is persisted only after a successful send. - The createSelfConversationMetadata migration also strips connections/ timezone out of any DBMyProfile.metadata written by the interim global routing, so polluted dev installs stop broadcasting them. - Scoped rows cascade on conversation delete and are cleared by wipeAccountScopedRows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address PR review - clear scoped rows in clear(), document persist-after-send ordering GRDBSelfProfileStore.clear() now deletes the inbox's scoped-metadata rows alongside the profile row, matching the in-memory implementation so the store contract holds (no production caller today, but a future one must not resurface a cleared account's grants). Contract test extended. publishScopedMetadata keeps its send-then-persist ordering; the doc comment now spells out why both alternatives (persist-before-send, non-fatal persist failure) leave worse end states. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: propagate metadata clears to receivers; thread inbox id through scoped store Macroscope re-review round two. The High: clearing the last scoped key produced a ProfileUpdate with no metadata field, which post-cutover receivers treated as "no change" - a revoked grant survived forever on the receiver. The wire cannot distinguish an empty map from an absent one (proto3 map), but every wire client re-sends its full map on each update - the legacy receivers replaced the stored map wholesale and the CLI/agents were built against that (serve.ts merges existing metadata into every send). The unified-profile cutover silently changed this to keep-on-absent. Restore replace-including-clear semantics for the addressed ProfileUpdate paths (stream, NSE, history replay): a winning update's non-nil map replaces the stored one and an empty map clears it, while nil (snapshot/appData fill paths) still says nothing. The recency guard keeps an older replay from clearing newer data. The Medium: the scoped-metadata store resolved the self inbox id through its own provider, so a transiently nil answer mid-drain could silently drop a conversation's scoped keys from a queued send. The scoped store methods now take the caller's already-resolved inbox id explicitly, matching the ProfileStore style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: scope empty-map metadata clears to scoped keys; tombstone the clear Macroscope round three, both findings valid against the previous replace-including-clear change: An omitted metadata map decodes identically to an empty one (proto3), so treating an empty map as a wholesale clear let a name-only update from any client that does not re-send its map wipe unrelated stored metadata, including the agent attestation. A winning update's empty map now clears only the conversation-scoped keys (new shared ConversationScopedMetadataKey: connections, timezone) - the keys whose senders always publish their full current state - and leaves everything else untouched. A clear stored as nil was indistinguishable from never-known, so the fill-blank path could resurrect revoked grants from a stale snapshot. The clear now leaves an empty-map tombstone (mirroring the avatar-slot tombstone), which fill paths treat as known-and-empty. Covered by new merge- and applier-level tests: scoped-only clearing preserves an attestation through a rename, and the tombstone blocks snapshot resurrection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Replace per-conversation DBMemberProfile with canonical DBProfile for unified identity storage
DBProfile,DBProfileAvatar, and related tables to store per-inbox identity and per-conversation avatar slots, replacing scatteredDBMemberProfilelookups across messaging, streaming, and contact sync.ProfilesRepository,ProfilePublisher,ProfileInboundApplier, andProfileMergeto manage reactive profile reads, durable publish jobs, and precedence-based identity merges.MessagingServiceandStreamProcessornow delegate toProfileInboundApplier.applyinstead of inlineDBMemberProfilewrites.InboxProfileAvatarViewis introduced so contact rows and conversation avatars observe the unified profile stream by inboxId and update live.DBMemberProfiletoDBProfileAvatar/DBProfile.ProfileBackfillmigrates existingDBMemberProfilerows into the new canonical tables on service start.selfProfiletable, newconversationLocalStatecolumn); existing databases are upgraded in place viaSharedDatabaseMigrator.Macroscope summarized 14ea335.