From 68456002283ef6b4a5b8d80d7172ab9e2ecfa3be Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:16:51 +0000 Subject: [PATCH] fix(identity): Mine includes unattributed and channel-only threads Mine/Ours now keeps threads with no person tags (legacy, channel-only stamps like desktop, identity-disabled servers). Theirs is only threads that have person attribution excluding the session claim. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .../client-runtime/src/state/identity.test.ts | 54 ++++++++++++++++--- packages/client-runtime/src/state/identity.ts | 39 ++++++++++---- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/client-runtime/src/state/identity.test.ts b/packages/client-runtime/src/state/identity.test.ts index 8165b4f595f2..733878119957 100644 --- a/packages/client-runtime/src/state/identity.test.ts +++ b/packages/client-runtime/src/state/identity.test.ts @@ -63,7 +63,7 @@ describe("filterPeopleForTypeahead", () => { }); describe("threadMatchesMine", () => { - it("filters mine vs theirs", () => { + it("filters mine vs theirs when threads are person-attributed", () => { expect( threadMatchesMine({ claimPersonId: "patroza", @@ -87,26 +87,66 @@ describe("threadMatchesMine", () => { ).toBe(true); }); - it("excludes both mine and theirs when there is no claim for the env", () => { + it("treats unattributed threads as mine (legacy / channel-only / identity off)", () => { + // No origin or participants — old threads, { channel: "desktop" } only, etc. expect( threadMatchesMine({ - claimPersonId: null, - originPersonId: "patroza", + claimPersonId: "patroza", + originPersonId: null, + participantPersonIds: [], mode: "mine", }), + ).toBe(true); + expect( + threadMatchesMine({ + claimPersonId: "patroza", + originPersonId: null, + participantPersonIds: [], + mode: "theirs", + }), ).toBe(false); + // Identity-disabled env: no claim, no person tags → Mine still shows work. expect( threadMatchesMine({ claimPersonId: null, - originPersonId: "patroza", + originPersonId: undefined, + participantPersonIds: undefined, + mode: "mine", + }), + ).toBe(true); + expect( + threadMatchesMine({ + claimPersonId: null, + originPersonId: undefined, mode: "theirs", }), ).toBe(false); + }); + + it("treats attributed threads without a session claim as theirs", () => { expect( threadMatchesMine({ claimPersonId: null, - originPersonId: "patroza", - mode: "any", + originPersonId: "julius", + mode: "mine", + }), + ).toBe(false); + expect( + threadMatchesMine({ + claimPersonId: null, + originPersonId: "julius", + mode: "theirs", + }), + ).toBe(true); + }); + + it("includes participant-only matches as mine", () => { + expect( + threadMatchesMine({ + claimPersonId: "patroza", + originPersonId: "julius", + participantPersonIds: ["patroza"], + mode: "mine", }), ).toBe(true); }); diff --git a/packages/client-runtime/src/state/identity.ts b/packages/client-runtime/src/state/identity.ts index b52a4b886b88..73edfe00dd00 100644 --- a/packages/client-runtime/src/state/identity.ts +++ b/packages/client-runtime/src/state/identity.ts @@ -73,7 +73,18 @@ export function filterPeopleForTypeahead( }); } -/** Match a thread as "mine" against the session claim personId. */ +/** + * Match a thread for Mine / Theirs ownership filters. + * + * **Mine** includes: + * - threads where the session claim person appears on origin or participants + * - threads with **no person attribution** (no identity tags, channel-only + * stamps like `{ channel: "desktop" }`, identity-disabled servers, legacy + * threads) — treated as "ours" so filters stay useful offline of a map + * + * **Theirs** is only threads that have at least one person tag and do not + * include the claim person. + */ export function threadMatchesMine(input: { readonly claimPersonId: string | null | undefined; readonly originPersonId?: string | null | undefined; @@ -81,16 +92,26 @@ export function threadMatchesMine(input: { readonly mode: "mine" | "theirs" | "any"; }): boolean { if (input.mode === "any") return true; - const claimId = input.claimPersonId?.trim().toLowerCase() ?? ""; - // No claim for this environment (map off, or user never signed up there): - // ownership is unclassifiable — hide from both Mine and Theirs. Multi-env - // clients with primary=smart (no map) previously used a single empty claim - // and treated every thread as Theirs, which made Mine look broken for t3vm. - if (claimId.length === 0) return false; + const people = new Set(); - if (input.originPersonId) people.add(input.originPersonId.trim().toLowerCase()); + const origin = input.originPersonId?.trim().toLowerCase() ?? ""; + if (origin.length > 0) people.add(origin); for (const id of input.participantPersonIds ?? []) { - people.add(id.trim().toLowerCase()); + const personId = id?.trim().toLowerCase() ?? ""; + if (personId.length > 0) people.add(personId); + } + const unattributed = people.size === 0; + + // No person tags (channel-only source, identity off, pre-attribution history). + if (unattributed) { + return input.mode === "mine"; + } + + const claimId = input.claimPersonId?.trim().toLowerCase() ?? ""; + // Attributed threads need a claim to classify as mine; without a claim they + // are someone else's tags on a map-enabled env (or another person's work). + if (claimId.length === 0) { + return input.mode === "theirs"; } const isMine = people.has(claimId); return input.mode === "mine" ? isMine : !isMine;