# Notifications Debug Log — 2026-05-28

Investigator: claude-coder (claude-sonnet-4-6)

---

## 1. DB existence check

**Checked:** `ls -la ~/.infinibot/notifications/` → **directory does not exist**
**Found:** History DB was never created. The store is lazily initialized on first call to `getNotificationHistoryStore()`.
**Root cause:** No notification has ever been emitted, so `emitNotification` (which calls the store) has never run. Discipline alerts only fire at scheduled times (09:30 / 21:00). The sibling emitter agent (`coder-notification-emitters-agents-tasks-di-xn2e`) is still wiring agent/kanban event sites.
**Not a bug in itself** — the DB will be created on first call to any `notifications.history.*` RPC. Zero rows until emitters fire.

---

## 2. `emitNotification` existence & broadcaster wiring

**Checked:** `src/notifications/emit.ts` — `emitNotification` is exported, canonical channel is `notification.created`.
**Found:** `setNotificationBroadcaster` is imported and **called at line 363** of `src/gateway/server.impl.ts` alongside `setTerminalBroadcaster`, `setUIControlBroadcaster`, and `setSubagentBroadcaster`. ✅ Server-side broadcast wiring is **correct**.
**Found:** `notificationHandlers` is imported and spread into `coreGatewayHandlers` at line 1118 of `src/gateway/server-methods.ts`. ✅ All notification RPC methods are **registered**.
**Found:** `notifications.test` admin handler exists in `src/gateway/server-methods/notifications.ts`. ✅ Test-fire path exists.

---

## 3. Gateway event broadcast path

**Checked:** `src/gateway/server.impl.ts:363` — `setNotificationBroadcaster(broadcast)` called at gateway boot. ✅
**Checked:** `emit.ts` — broadcaster fires `notification.created` with `{ id, ts, category, severity, title, body, source, sourceId, deepLink, data }` shape.
**Found:** No mismatch in channel name — server broadcasts `notification.created`, mobile subscribes to `notification.created`. ✅

---

## 4. `notifications.history.list` shape vs mobile expectations

**Checked:** Server returns `{ items: NotificationEntry[], nextCursor: string | null, total: number }`.
**Checked:** `useNotifications.js` reads `res?.items || []`. ✅ Shape matches.
**Checked:** `NotificationsCenterScreen.js` reads `res?.items || []`. ✅ Shape matches.
**Found:** `NotificationEntry` fields include `id`, `ts`, `category`, `severity`, `title`, `body`, `source`, `sourceId`, `readByToken[]`, `dismissedByToken[]`, `ackByToken[]`. Mobile uses all these fields correctly.

---

## 5. Mobile hook `useNotifications.js` — BUG FOUND

**Checked:** `useNotifications.js:369-374`:
```js
useEffect(() => {
  if (expoPushToken && sendToGateway) {   // ← BUG: gated on push token
    loadFromGateway(true);
    loadPreferences();
  }
}, [expoPushToken, sendToGateway]);
```
**Bug A:** Initial load from gateway is gated on `expoPushToken` being set. In dev/simulator environments or when push permission is denied, `expoPushToken` stays `null` permanently → `loadFromGateway` never called → history feed stays empty.
**Fixed:** Changed to `if (sendToGateway)` with `expoPushToken || '_default_'` token fallback.

**Bug B:** No WebSocket subscription to `notification.created` in the hook. The `notifications` state and `unreadCount` never update live. `NotificationBanner` and `NotificationsCenterScreen` subscribe independently via `gateway.subscribeEvent`, but the hook's state (used for badge counts etc.) is stale.
**Fixed:** Added `subscribeEvent` param; hook now subscribes to `notification.created` and prepends new entries.

**Bug C:** `loadFromGateway` has `notifications` in its `useCallback` deps, causing infinite recreation risk. Using functional `setState` pattern to eliminate that dep.
**Fixed:** Rewrote `loadFromGateway` to use functional setState; removed `notifications` from deps.

---

## 6. `NotificationsCenterScreen.js` — BUGs FOUND

**Bug D:** Auto-poll `useEffect` has empty `[]` deps — captures stale `loadItems`/`loadStats` from initial render:
```js
useEffect(() => {
  pollRef.current = setInterval(() => {
    loadStats();   // ← stale closure
    loadItems(true);  // ← stale closure
  }, 15000);
  return () => clearInterval(pollRef.current);
}, []);  // BUG: no deps
```
**Fixed:** Replaced with stable function refs (`loadItemsRef`, `loadStatsRef`) updated on every render.

**Bug E:** `critCount` always returns 0:
```js
const critCount = (todaySummary?.summary || [])
  .filter((_) => true)
  .reduce((s, _) => s, 0);  // ← reduce body ignores accumulator!
```
`byCategorySummary` returns category-grouped data; there's no severity breakdown. Also the reduce is a no-op.
**Fixed:** Added a separate `notifications.history.list` call with `severities: ['critical', 'error']` and `fromTs: today` to get an accurate critical+error count via `total`.

---

## 7. Live update path end-to-end

**Verified:** `gateway.subscribeEvent('notification.created', handler)` in `NotificationsCenterScreen.js:395-412` → correct.
**Verified:** `NotificationBanner.js:177-181` subscribes to `notification.created` via `gateway.subscribeEvent` → correct.
**Verified:** `useGateway.js:935` — `subscribeEvent` is in the returned object and is passed as `gateway.subscribeEvent` to both components.

---

## 8. App.js wiring

**Found:** `useNotifications` called at App.js:1049 only with `{ sendToGateway: gateway.sendReq, ... }` — `gateway.subscribeEvent` is NOT passed.
**Fixed:** Added `subscribeEvent: gateway.subscribeEvent` to the `useNotifications` call.

---

## Summary of root causes

| # | Bug | Severity | Fixed |
|---|-----|----------|-------|
| A | `useNotifications` never loads if no Expo push token | Critical | ✅ |
| B | No WS subscription in `useNotifications` — live state stale | Critical | ✅ |
| C | `loadFromGateway` captures `notifications` in deps | Medium | ✅ |
| D | Stale closure in 15s auto-poll | Medium | ✅ |
| E | `critCount` always 0 | Low | ✅ |
| — | No emitters wired yet (sibling agent still running) | External | TODO for emitter agent |

---

## TODOs for still-running emitter agent (`coder-notification-emitters-agents-tasks-di-xn2e`)

The notification framework is fully functional server-side. Once the emitter agent wires call sites:
1. `emitNotification({ category: 'agent.completed', ... })` in agent completion handlers
2. `emitNotification({ category: 'kanban.task.assigned', ... })` in kanban writes
3. `emitNotification({ category: 'autonomy.checkpoint', ... })` in autonomy events
4. `emitNotification({ category: 'cron.fired', ... })` in cron execution
5. Test via: `gateway.sendReq('notifications.test', { category: 'manual', severity: 'info' })`

The history store will be created automatically on first emit.
