Found while implementing #4197 (PR #4223). Not fixed there — out of scope for that card, which covers the approvals and sys_activity feeds.
What
On /home two consumers mount in one tree and each issues its own sys_inbox_message read:
| consumer |
query |
cadence |
bell poll — layout/AppHeader.tsx |
$filter: {user_id}, $orderby: {created_at: desc}, $top: 20, joined with sys_notification_receipt ($top: 200) for read-state |
every 10s |
hooks/useHomeInbox.ts → Home's To-do card |
$filter: {user_id}, $orderby: {created_at: desc}, $top: limit (default 5) |
once on mount |
So a Home page load issues both, and the bell's copy repeats every 10 seconds.
Why it exists
Pre-existing, and a side effect of the fix sequence rather than of any one change. #4199 un-gated the bell's inbox poll so the Notifications tab would fill in off-app — correctly — but did not merge it with Home's existing read. #4197 then shared the other two feeds (approvals and sys_activity) through hooks/sharedUserFeeds.ts; the inbox read was deliberately left out because the two consumers ask genuinely different questions there, unlike the other two feeds where the queries were byte-identical.
Observation-class, not a defect
Both consumers show correct data today. Nothing is stale, nothing disagrees — the badge's unread arithmetic lives entirely on the bell's side, and Home's card only lists titles. The cost is one extra round trip per Home load plus a 10s poll that partly overlaps it. Filing it so the duplication is recorded rather than rediscovered, not because a user is currently hitting a bug.
Why it is not a trivial merge
Home's rows are a strict prefix of the bell's superset (same object, same filter, same ordering, smaller $top), so one read could serve both. But:
- the bell additionally reads
sys_notification_receipt and joins it for is_read; Home needs none of that;
- the bell maps to a row shape carrying
notification_id / receipt_id for mark-read, Home to {id, title, actionUrl, createdAt} with a title-dedupe pass;
$top differs (20 vs limit), so a shared store would have to hold the superset and let Home slice — which is exactly the shape sharedUserFeeds already uses for activity, where Home filters the bell's rows at its own call site.
That last point is the suggested route: extend sharedUserFeeds with an inbox feed holding the bell's 20 merged rows, have Home derive its five from them. The merge is mechanical but it touches mark-read, so it deserves its own change and its own tests rather than riding along.
Pointers
Found while implementing #4197 (PR #4223). Not fixed there — out of scope for that card, which covers the approvals and
sys_activityfeeds.What
On
/hometwo consumers mount in one tree and each issues its ownsys_inbox_messageread:layout/AppHeader.tsx$filter: {user_id},$orderby: {created_at: desc},$top: 20, joined withsys_notification_receipt($top: 200) for read-statehooks/useHomeInbox.ts→ Home's To-do card$filter: {user_id},$orderby: {created_at: desc},$top: limit(default 5)So a Home page load issues both, and the bell's copy repeats every 10 seconds.
Why it exists
Pre-existing, and a side effect of the fix sequence rather than of any one change. #4199 un-gated the bell's inbox poll so the Notifications tab would fill in off-app — correctly — but did not merge it with Home's existing read. #4197 then shared the other two feeds (approvals and
sys_activity) throughhooks/sharedUserFeeds.ts; the inbox read was deliberately left out because the two consumers ask genuinely different questions there, unlike the other two feeds where the queries were byte-identical.Observation-class, not a defect
Both consumers show correct data today. Nothing is stale, nothing disagrees — the badge's unread arithmetic lives entirely on the bell's side, and Home's card only lists titles. The cost is one extra round trip per Home load plus a 10s poll that partly overlaps it. Filing it so the duplication is recorded rather than rediscovered, not because a user is currently hitting a bug.
Why it is not a trivial merge
Home's rows are a strict prefix of the bell's superset (same object, same filter, same ordering, smaller
$top), so one read could serve both. But:sys_notification_receiptand joins it foris_read; Home needs none of that;notification_id/receipt_idfor mark-read, Home to{id, title, actionUrl, createdAt}with a title-dedupe pass;$topdiffers (20 vslimit), so a shared store would have to hold the superset and let Home slice — which is exactly the shapesharedUserFeedsalready uses for activity, where Home filters the bell's rows at its own call site.That last point is the suggested route: extend
sharedUserFeedswith an inbox feed holding the bell's 20 merged rows, have Home derive its five from them. The merge is mechanical but it touches mark-read, so it deserves its own change and its own tests rather than riding along.Pointers
packages/app-shell/src/layout/AppHeader.tsx— the polled read (the effect documented "Poll the signed-in user's in-app inbox (ADR-0030 L5)")packages/app-shell/src/hooks/useHomeInbox.ts— the one-shot readpackages/app-shell/src/hooks/sharedUserFeeds.ts— the store the other two feeds already share, added by fix(app-shell): the bell's Approvals and Activity tabs fill in off-app, from one shared fetch (#4197) #4223