Skip to content

The bell's inbox poll latches itself off permanently on one 404, while Home's card reading the same object does not #4289

Description

@yinlianghui

Observation-class, found while re-checking #4230's premise (PR #4284). Not a defect anyone is demonstrably hitting today — filing it because it is a second, independent way to produce the exact #4110 / #4230 signature, and the next dead-bell report should have it on the list instead of rediscovering it.

The asymmetry

Two consumers read sys_inbox_message with the same filter for the same user. Only one of them can turn itself off for good.

packages/app-shell/src/layout/AppHeader.tsx — the bell's 10s poll:

const notificationsUnavailableRef = useRef(false);

useEffect(() => {
  if (!dataSource || !user?.id) return;
  if (notificationsUnavailableRef.current) return;   // permanent, page-lifetime
  ...
  } catch (err: any) {
    if (isMissingResource(err)) {
      notificationsUnavailableRef.current = true;    // latched — never cleared
      return;
    }

isMissingResource is err?.httpStatus === 404 || err?.status === 404 || errorCodeIs(err, 'OBJECT_NOT_FOUND'). Nothing clears the ref: not a dep change, not visibilitychange, not a later successful read. One qualifying error at any point and the bell holds [] until the page is reloaded.

packages/app-shell/src/hooks/useHomeInbox.ts — Home's "Needs your attention" card, same object, same $filter: { user_id: user.id }, same $orderby:

  .catch(() => { /* inbox pipeline absent → empty */ });

No ref, no latch. It re-runs whenever [dataSource, user?.id, limit] settle, so a transient failure costs it one render, not the session.

Why it is worth recording

The latch is deliberate and its reasoning is sound as written — the comment explains that re-requesting an absent optional collection on every navigation is console noise plus wasted round trips, and sharedUserFeeds carries the same rule for approvals and sys_activity. The part that is not stated is that the latch's blast radius is different for this feed than for those, because this feed has a sibling consumer that does not latch. The observable result of a single transient missing-resource answer during bootstrap is:

  • bell panel empty under both Unread and All,
  • Home's To-do card listing the same rows correctly,
  • zero requests issued when the panel is opened.

That is the #4110 / #4230 three-way contrast exactly, produced by a completely different cause than the isApp gate that produced it the first two times. Two cards have now been triaged against that signature, and both times the reasoning went "home card works, so the query and the user id are fine, so the drop is downstream" — a chain that this latch breaks silently.

What is NOT claimed

I have not measured that a qualifying 404 is reachable during normal bootstrap. The code path is real and readable; whether the adapter can answer OBJECT_NOT_FOUND for sys_inbox_message on a deployment that does have the object — for instance before metadata settles — is unverified, and that is the whole question of whether this is a latent defect or merely an asymmetry. Establishing that is the first step for whoever picks it up; if it is unreachable, this closes as a comment on the design and costs nothing.

Note the receipt read is not the exposure: sys_notification_receipt is wrapped in .catch(() => ({ data: [] })), so its absence degrades to "everything unread" as intended. The exposure is a rejection from the sys_inbox_message read itself, or a synchronous throw from either find call before the .catch is attached.

If it is reachable, the shapes worth weighing

  1. Clear the latch on a dep change (user?.id / dataSource), so a session that recovers gets its bell back without a reload. Smallest change; keeps the noise argument intact for the genuinely-absent case.
  2. Latch only after N consecutive missing-resource answers, distinguishing "this deployment has no messaging pipeline" from "one read lost a race". Closer to what the comment actually intends.
  3. Leave it and pin it — assert the latch is a deliberate one-way door, and make the bell say so rather than rendering an empty inbox indistinguishable from a genuinely empty one. A user staring at "You're all caught up" is being told something false.

Adjacent, not the same: #4225 proposes folding this read into hooks/sharedUserFeeds.ts. Whoever does that inherits this decision — the shared store would give the bell and Home one retry policy, which resolves the asymmetry by construction and makes the choice above unavoidable rather than optional.

Pointers

Related: #4230, #4110, #4156, #4199, #4225, #4235.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions