Found while implementing #4235 (PR #4315). Not fixed there: that card is the
false-negative half (an unanswered read rendering "You're all caught up"), this
is the false-positive mirror, and closing it means adding a second object read.
What
packages/app-shell/src/hooks/useHomeInbox.ts reads sys_inbox_message
filtered by user_id, newest first, $top: limit — and nothing else. It
never reads sys_notification_receipt.
Read-state is not on the message row. ADR-0030 resolved design decision 2 says
so explicitly — "Read-state lives in sys_notification_receipt (per
recipient×channel), not on sys_inbox_message" — and the shipped object
confirms it: packages/services/service-messaging/src/objects/inbox-message.object.ts
declares no read column (the pre-convergence read field named in the ADR's
drift section is gone).
So Home's "Needs your attention" card cannot tell a read message from an unread
one. It lists the five most recent messages unconditionally and badges their
count.
Consequence
A user who opens the bell and reads all nine messages, then returns to Home,
still sees up to five of them listed under "Needs your attention" with a badge —
work that is not waiting on them. The bell, two hundred pixels above, correctly
shows zero unread, because its poll DOES join the receipts
(packages/app-shell/src/layout/AppHeader.tsx, READ_STATES /
receiptByNotif). Two panels on one page, one page load, disagreeing about the
same rows.
This is the exact mirror of #4235: that card was the panel claiming nothing is
waiting when something is; this is the panel claiming something is waiting when
nothing is. #4235's fix does not touch it — it gates the affirmative empty copy
on the read having answered, which is orthogonal to what the answered rows mean.
Why it is not fixed in PR #4315
Adding the receipt join to useHomeInbox is a second read of a second object on
every Home load — which is precisely what #4225 exists to prevent, and #4225's
own suggested route is the opposite direction: extend hooks/sharedUserFeeds.ts
with an inbox feed holding the bell's already-joined 20 rows and let Home derive
its five from them, paying zero extra round trips. That route is the right fix
for this and it is already scoped to #4225, which also warns that touching it
must not re-break the bell (#4230).
Filing separately rather than as a sub-issue of #4225: #4225 is explicitly
observation-class ("Both consumers show correct data today. Nothing is stale,
nothing disagrees") and this contradicts that premise, so it is evidence #4225
should be re-graded rather than a task inside its current scope. If #4225 is
re-scoped to the shared inbox feed, this becomes a sub-issue of it.
Pointers
packages/app-shell/src/hooks/useHomeInbox.ts — the unjoined read
packages/app-shell/src/layout/AppHeader.tsx — the joined read, for contrast
packages/app-shell/src/hooks/sharedUserFeeds.ts — where the merged rows would live
- objectstack
docs/adr/0030-notification-platform-convergence.md — resolved decision 2
Generated by Claude Code
Found while implementing #4235 (PR #4315). Not fixed there: that card is the
false-negative half (an unanswered read rendering "You're all caught up"), this
is the false-positive mirror, and closing it means adding a second object read.
What
packages/app-shell/src/hooks/useHomeInbox.tsreadssys_inbox_messagefiltered by
user_id, newest first,$top: limit— and nothing else. Itnever reads
sys_notification_receipt.Read-state is not on the message row. ADR-0030 resolved design decision 2 says
so explicitly — "Read-state lives in
sys_notification_receipt(perrecipient×channel), not on
sys_inbox_message" — and the shipped objectconfirms it:
packages/services/service-messaging/src/objects/inbox-message.object.tsdeclares no
readcolumn (the pre-convergencereadfield named in the ADR'sdrift section is gone).
So Home's "Needs your attention" card cannot tell a read message from an unread
one. It lists the five most recent messages unconditionally and badges their
count.
Consequence
A user who opens the bell and reads all nine messages, then returns to Home,
still sees up to five of them listed under "Needs your attention" with a badge —
work that is not waiting on them. The bell, two hundred pixels above, correctly
shows zero unread, because its poll DOES join the receipts
(
packages/app-shell/src/layout/AppHeader.tsx,READ_STATES/receiptByNotif). Two panels on one page, one page load, disagreeing about thesame rows.
This is the exact mirror of #4235: that card was the panel claiming nothing is
waiting when something is; this is the panel claiming something is waiting when
nothing is. #4235's fix does not touch it — it gates the affirmative empty copy
on the read having answered, which is orthogonal to what the answered rows mean.
Why it is not fixed in PR #4315
Adding the receipt join to
useHomeInboxis a second read of a second object onevery Home load — which is precisely what #4225 exists to prevent, and #4225's
own suggested route is the opposite direction: extend
hooks/sharedUserFeeds.tswith an inbox feed holding the bell's already-joined 20 rows and let Home derive
its five from them, paying zero extra round trips. That route is the right fix
for this and it is already scoped to #4225, which also warns that touching it
must not re-break the bell (#4230).
Filing separately rather than as a sub-issue of #4225: #4225 is explicitly
observation-class ("Both consumers show correct data today. Nothing is stale,
nothing disagrees") and this contradicts that premise, so it is evidence #4225
should be re-graded rather than a task inside its current scope. If #4225 is
re-scoped to the shared inbox feed, this becomes a sub-issue of it.
Pointers
packages/app-shell/src/hooks/useHomeInbox.ts— the unjoined readpackages/app-shell/src/layout/AppHeader.tsx— the joined read, for contrastpackages/app-shell/src/hooks/sharedUserFeeds.ts— where the merged rows would livedocs/adr/0030-notification-platform-convergence.md— resolved decision 2Generated by Claude Code