use-counterpart-profile.tsfix: six review findings — tenant leaks, wrong seat, lost writes
The LinkedIn counterpart cache was GLOBAL. Keyed on `provider_urn` alone, one row served every
customer who had met that member: the second org's refresh overwrote the first org's row and its
`person_id` (a pointer into that org's own crm_person), and either org's read could hand back the
other's cache. The key is now (organization_id, provider_urn), and the read and the upsert both
say so. DDL applied as a targeted, idempotent ALTER — the old single-column PK is dropped only
where it is still the one in place, and the org index goes with it, being redundant the moment the
key leads with the org.
The refresh no longer takes a seat from its caller. A profile view is billed to one seat's 50/day
and reaches LinkedIn as that seat's identity, and the client was naming the org's first connected
account — so in an org with several seats the panel spent the wrong rep's budget under the wrong
rep's name, with nothing checking the seat had any relationship to the chat. `linkedin_chats`
already records which seat holds a chat, so that is where the answer comes from.
A remembered `budget` skip now expires. The client suppresses its auto-refresh entirely while one
is reported, so an unexpiring reason was a permanently cold panel: a chat opened once while the
day's views were down to the send reserve never refreshed on its own again, however many days
later it was reopened. `last_skip_at` stamps the reason and the read honours it only inside the
window it was measured in — windows read off the governor's own caps, not a number repeated here.
A range or paste over two linked cells of ONE deal now writes them in order. `crm.updateConversation`
holds an optimistic lock on the conversation's `updated_at`; fired together, both calls read the
same timestamp, the first commits and the second matches no row and comes back CONFLICT — so half
the gesture reverted with "please retry" on an edit nobody was competing for. Same deal, one at a
time; different deals still in parallel.
"Shared with me" is gone. It listed every row in the org owned by another PERSON, which is not a
sharing relationship — there is none between users in this product; `share.ts` is public token
links. `user/` is the one genuinely private scope and `documents.get` refuses to open another
person's row at all, so the view could only ever show teammates' private files (10,037 rows live,
titles, paths and a 300-character content preview included) and then fail to open them. A menu row
that cannot be implemented honestly is worse than no menu row.
The Deleted view now lists exactly what Restore accepts. It listed every org-scoped tombstone,
which put 52 live rows (`#agendas/…`, `organisation/wiki/…`, `organisation/tables/…`) on screen
wearing a button that `callerOwnsDocument` always refuses. The set predicate lives beside that row
check so the two cannot drift apart. One deliberate gap: a `thread/` row with no owner is left out
rather than listed, because its ownership lives on the thread and cannot be asked in the query.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 12:03 PM