Skip to content

feat(notifications): core→shell DomainEvent bridge (Phase 1d of #395)#782

Merged
senamakel merged 5 commits intotinyhumansai:mainfrom
jwalin-shah:feat/notification-bridge
Apr 23, 2026
Merged

feat(notifications): core→shell DomainEvent bridge (Phase 1d of #395)#782
senamakel merged 5 commits intotinyhumansai:mainfrom
jwalin-shah:feat/notification-bridge

Conversation

@jwalin-shah
Copy link
Copy Markdown
Contributor

@jwalin-shah jwalin-shah commented Apr 22, 2026

Summary

Stacked on #780 and #769. Closes the last open chunk of the notification work in #395: piping Rust-core DomainEvents into the in-app notification center, so cron completions / webhook failures / sub-agent completions surface alongside the chat + webview events already wired.

Design

Reuses the existing Socket.IO transport — no new RPC surface. Matches the overlay / dictation bridge pattern that already broadcasts to all clients.

Rust (src/openhuman/notifications/)

  • types.rsCoreNotificationEvent { id, category, title, body, deep_link, timestamp_ms }, shape matches frontend NotificationItem
  • bus.rs — broadcast channel + NotificationBridgeSubscriber implementing EventHandler. Pure event_to_notification(&DomainEvent) translator, unit-tested
  • Hooked into core::socketio::spawn_web_channel_bridge as a 5th bridge task
  • Registered in register_domain_subscribers() (Once-guarded)

Variants surfaced

DomainEvent Category Notes
CronJobCompleted agents Different title for success vs failure
WebhookProcessed system Silent unless status >= 400 or error.is_some() — successful webhooks are noisy
SubagentCompleted agents Body includes agent_id + output_chars

Frontend (app/src/lib/nativeNotifications/service.ts)

  • New listener on core_notification socket event
  • Dispatches into notificationSlice with the server-provided category + deep link
  • Reuses the same focus/preference gating as chat events

Merge order

Stacked: #769#780this. After each merges the next will auto-rebase.

Test plan

  • Rust: 6 unit cases (cron pass/fail, silent webhook, failed webhook, subagent, unrelated) — cargo test openhuman::notifications green
  • Vitest: 14/14 across slice + service (2 new for core_notification routing)
  • tsc --noEmit + eslint + cargo check clean
  • Manual: trigger a failing webhook / finished cron / subagent completion; verify banner (unfocused) + center item

Remaining for #395

Phase 2 polish — DND / quiet hours, sound customization, Windows/Linux-specific tweaks. Not blocking; punt until we see how 1a–1d feel in use.

Summary by CodeRabbit

Release Notes

  • New Features
    • Core notifications system added: The app now sends and displays real-time notifications for key system events, including cron job completion, webhook processing, and agent activities. Notifications are delivered reliably with proper categorization and deduplication.

…nsai#395)

Adds persistent notification center that captures events from the existing
webview notification surface shipped in tinyhumansai#741.

- notificationSlice (Redux + redux-persist) with categories + preferences
- /notifications route + Notification Center page (read/unread, click-to-navigate, mark all, clear)
- Bell tab in BottomTabBar with unread badge
- NotificationsPanel in Settings → Features for per-category toggles (messages/agents/skills/system)
- Webview notification handler dispatches into the center in addition to bumping account unread
- Tests: 7 reducer cases covering prepend, preference gating, read flips, cap, unread count

Next: PR B wires core-side native banners for agent/skill/channel events (tauri-plugin-notification).
…se 1c of tinyhumansai#395)

Stacked on tinyhumansai#769. Adds OS-level notifications for events the frontend
already sees over the existing socket.io + Tauri event surface, so the
notification center earns its keep even when the app is backgrounded.

- Tauri command `show_native_notification(title, body, tag?)` using the
  already-installed `tauri-plugin-notification`
- Frontend `nativeNotifications` service subscribes to socket events:
    - `chat_done`   → agents category
    - `chat_error`  → system category
    - `disconnect`  → system category
- Only fires the OS banner when the window is NOT focused — in-app
  center covers the focused case
- Respects per-category preferences from the settings panel
- Vitest: 5 cases (dispatch, truncation, preference gating, focus gate,
  unfocused banner fire)

Skipped the core→shell DomainEvent bridge for this PR: the Rust core
runs in a separate process so piping DomainEvents into the Tauri shell
would need a new event-relay transport. Existing socket events already
carry agent completions, so this PR banner-izes them without new Rust
plumbing. Bridge lands in a follow-up if we need cron/webhook/skill-sync
banners too.
…umansai#395)

Stacked on tinyhumansai#780. Adds a core-side notification bus that subscribes to
selected DomainEvents and fans them out over the existing Socket.IO
transport as `core_notification` / `core:notification` events — no new
wire-level transport or RPC surface needed.

New module `src/openhuman/notifications/`:
- `types.rs` — `CoreNotificationEvent` (id, category, title, body,
  deep_link, timestamp_ms) matching the frontend slice shape
- `bus.rs` — broadcast channel + `NotificationBridgeSubscriber`
  implementing `EventHandler`. Pure `event_to_notification(&DomainEvent)`
  translator, unit-tested.

Surfaces:
- `DomainEvent::CronJobCompleted` → agents category (success + failure
  variants)
- `DomainEvent::WebhookProcessed` → system category (only when HTTP
  status ≥ 400 or an error was set — successful webhooks are silent)
- `DomainEvent::SubagentCompleted` → agents category

Wiring:
- `src/core/socketio.rs::spawn_web_channel_bridge` adds a 5th bridge
  task that subscribes to `notifications::subscribe_core_notifications`
  and broadcasts to all connected sockets (mirrors the overlay /
  dictation bridges).
- `src/core/jsonrpc.rs::register_domain_subscribers` registers the
  bridge subscriber alongside the existing webhook / channel / health
  ones, Once-guarded.

Frontend (`app/src/lib/nativeNotifications/service.ts`):
- New listener on the `core_notification` socket event dispatches into
  `notificationSlice` with the category + deep link delivered by the
  core. Banner path reuses the same focus/pref gating as chat events.

Tests:
- Rust: 6 unit cases over `event_to_notification` (cron pass, cron
  fail, successful webhook silent, failed webhook, subagent completed,
  unrelated event).
- Vitest: 2 new cases for core_notification routing (normal +
  missing-fields filter) — 14/14 total across the service + slice.
@jwalin-shah jwalin-shah requested a review from a team April 22, 2026 13:58
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@senamakel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 31 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f04f6536-add8-48b4-8d40-d5dd0a877bcb

📥 Commits

Reviewing files that changed from the base of the PR and between fcb5e4a and 3fa2682.

📒 Files selected for processing (4)
  • app/src/lib/nativeNotifications/service.ts
  • src/core/socketio.rs
  • src/openhuman/notifications/bus.rs
  • src/openhuman/notifications/types.rs
📝 Walkthrough

Walkthrough

A new core notification system has been implemented that bridges domain events to frontend clients via Socket.IO. The backend translates domain events into CoreNotificationEvent structures, broadcasts them through a dedicated bus, and forwards them to connected Socket.IO clients. The frontend receives these events and dispatches them as native notifications.

Changes

Cohort / File(s) Summary
Frontend Notification Service
app/src/lib/nativeNotifications/service.ts, app/src/lib/nativeNotifications/__tests__/service.test.ts
Added new core_notification socket event listener with validation of required fields (id, title), field truncation, and dispatch through existing notification flow. Exported test hook __handleCoreNotificationForTests and extended test utilities. Test coverage validates payload handling and rejection of incomplete payloads.
Backend Notification Types
src/openhuman/notifications/types.rs
Introduced CoreNotificationCategory enum and CoreNotificationEvent struct to define the wire format for core notifications, including serialization behavior and required fields (id, category, title, body, timestamp_ms) plus optional deep_link.
Backend Notification Bus
src/openhuman/notifications/bus.rs
Created new broadcast bus infrastructure with subscribe_core_notifications() and publish_core_notification() functions. Implemented NotificationBridgeSubscriber that translates selected DomainEvent variants into CoreNotificationEvent instances. Includes unit tests for event translation behavior.
Backend Notification Module
src/openhuman/notifications/mod.rs
Declared new bus sub-module and publicly re-exported core notification bridge APIs (subscribe_core_notifications, publish_core_notification, register_notification_bridge_subscriber, NotificationBridgeSubscriber).
Backend Socket.IO Integration
src/core/socketio.rs
Added notification bridge that subscribes to core notification events and forwards each event to all connected Socket.IO clients under "core_notification" and "core:notification" event names, with error handling and debug logging.
Backend Core Bootstrap
src/core/jsonrpc.rs
Registered notification bridge subscriber initialization in the core domain event-bus during bootstrap alongside existing health, channel, webhook, and other subscribers.

Sequence Diagram(s)

sequenceDiagram
    participant DE as Domain Event Source
    participant NB as Notification Bus
    participant SIO as Socket.IO Bridge
    participant Client as Frontend Client
    
    DE->>NB: emit DomainEvent (cron done, webhook done, etc.)
    activate NB
    NB->>NB: translate to CoreNotificationEvent
    NB->>SIO: broadcast CoreNotificationEvent
    deactivate NB
    
    activate SIO
    SIO->>Client: emit "core_notification" event
    deactivate SIO
    
    activate Client
    Client->>Client: validate (id, title required)
    Client->>Client: dispatch notification
    Client->>Client: maybe show banner
    deactivate Client
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A new bridge carries whispers through the air,
Domain dreams transform to notifications fair,
Socket streams them swift to browsers waiting near,
Bringing tidings front and center, loud and clear! 🔔

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing a bridge that pipes core DomainEvents to the shell notification system, with phase reference to track this work within the larger effort (#395).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Merged main into feat/notification-bridge. Resolved five conflict sites:

- app/src/lib/webviewNotifications/service.ts: use cached `now` variable
  from main rather than two calls to Date.now().
- app/src/pages/Settings.tsx: add NotificationRoutingPanel import and route
  that main introduced alongside this PR's changes.
- app/src/store/index.ts: add notificationsSlice / integrationNotifications
  reducer that main introduced alongside this PR's changes.
- app/src/components/settings/panels/NotificationsPanel.tsx: keep main's
  DND (Do Not Disturb) superset over the PR's simpler category-only version.
- app/src/lib/nativeNotifications/service.ts + test: merge PR's
  core_notification socket listener with main's listener-reference pattern
  for proper teardown via socketService.off().
- src/openhuman/notifications/types.rs + mod.rs: keep both the PR's
  CoreNotificationEvent bridge types and main's IntegrationNotification
  triage-pipeline types; expose both the bus and rpc/schemas submodules.

All checks pass: tsc --noEmit clean, ESLint 0 errors, Prettier unchanged,
cargo check clean (core + tauri), Vitest 548/548 tests green.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/core/socketio.rs (1)

239-245: ⚠️ Potential issue | 🟡 Minor

Update bridge count/list in function doc now that a fifth bridge was added.

spawn_web_channel_bridge doc comment still says "four bridges" and omits the new core_notification bridge. Also, the inline numbering becomes 1, 2, 3, 5, 4 because the new block at line 327 is labeled 5. while the transcription bridge below remains 4. — easy to misread when scanning.

✏️ Proposed fix
-/// This function sets up four bridges:
+/// This function sets up five bridges:
 /// 1. **Web Channel Bridge**: Forwards chat-related events (messages, tool calls) to specific clients.
 /// 2. **Dictation Bridge**: Forwards hotkey events to all clients.
 /// 3. **Overlay Bridge**: Forwards attention bubble events to all clients.
-/// 4. **Transcription Bridge**: Forwards real-time speech-to-text results to all clients.
+/// 4. **Core Notification Bridge**: Forwards core domain notifications to all clients.
+/// 5. **Transcription Bridge**: Forwards real-time speech-to-text results to all clients.

Either renumber the transcription block to 5. and the new block to 4., or reorder them. Either way, keep the numbering monotonic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/socketio.rs` around lines 239 - 245, The doc comment for
spawn_web_channel_bridge is out of sync: it still says "four bridges" and the
numbered list misorders the new core_notification bridge (introducing
1,2,3,5,4). Update the comment to reflect five bridges and make the numbering
monotonic (e.g., renumber the transcription bridge to 5 and the new
core_notification block to 4, or reorder the blocks) so the list reads 1–5
correctly and includes the new core_notification bridge.
src/core/jsonrpc.rs (1)

839-841: ⚠️ Potential issue | 🟡 Minor

Update subscriber list log to include notifications.

Line 813 registers the new notification bridge subscriber, but the summary log on line 840 still lists only the pre-existing subscribers. For grep-friendly tracing, add notifications to the list.

✏️ Proposed fix
         log::info!(
-            "[event_bus] domain subscribers registered (webhook, channel, health, conversation, composio, restart, proactive, agent)"
+            "[event_bus] domain subscribers registered (webhook, channel, health, notifications, conversation, composio, restart, proactive, agent)"
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/jsonrpc.rs` around lines 839 - 841, The event bus summary log
currently printed by the log::info! call that lists registered domain
subscribers is missing the newly added "notifications" subscriber; update the
log message string used in that log::info! invocation to include "notifications"
alongside the existing items (e.g., "...conversation, composio, restart,
proactive, agent, notifications") so the summary accurately reflects the
notification bridge subscriber registered elsewhere in the file.
🧹 Nitpick comments (2)
src/openhuman/notifications/bus.rs (1)

133-149: Add a tracing log on matched events for grep-friendly diagnostics.

handle() silently swallows non-matching events (intentional) and forwards matched ones to publish_core_notification, which logs at debug. A short trace/debug at the match site would help when tracing "why didn't this DomainEvent surface as a notification?" without cross-referencing publish_core_notification's log.

✏️ Optional addition
     async fn handle(&self, event: &DomainEvent) {
         if let Some(notification) = event_to_notification(event) {
+            log::trace!(
+                "{LOG_PREFIX} translated domain event → notification id={} category={:?}",
+                notification.id,
+                notification.category,
+            );
             publish_core_notification(notification);
         }
     }

As per coding guidelines: "add substantial development-oriented logs at entry/exit points, branch decisions, external calls, retries, state transitions".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/bus.rs` around lines 133 - 149, Add a
grep-friendly trace/debug log inside NotificationBridgeSubscriber::handle when
event_to_notification(event) yields Some(notification); specifically, log a
short message including the DomainEvent type/id or a succinct identifier and
that it matched before calling publish_core_notification, so you can trace
matched events without digging into publish_core_notification; update the handle
method to call the tracer (trace! or debug!) right after the if-let Some(...)
match and include event and/or notification identifying fields.
app/src/lib/nativeNotifications/service.ts (1)

106-177: Consider extracting shared core-notification handling to avoid duplication.

coreNotificationListener (Lines 106-115) and __handleCoreNotificationForTests (Lines 169-177) are near-identical implementations. If truncation limits, field mapping, or validation logic changes, both need to be updated in lockstep.

♻️ Proposed refactor
+  function handleCoreNotification(payload: CoreNotificationPayload): void {
+    if (!payload.id || !payload.title) return;
+    dispatchAndMaybeBanner(payload.category, {
+      id: payload.id,
+      title: truncate(payload.title, 120),
+      body: truncate(payload.body ?? '', 160),
+      deepLink: payload.deep_link ?? undefined,
+    });
+  }
+
   coreNotificationListener = (...args: unknown[]) => {
-    const p = (args[0] ?? {}) as CoreNotificationPayload;
-    if (!p.id || !p.title) return;
-    dispatchAndMaybeBanner(p.category, {
-      id: p.id,
-      title: truncate(p.title, 120),
-      body: truncate(p.body ?? '', 160),
-      deepLink: p.deep_link ?? undefined,
-    });
+    handleCoreNotification((args[0] ?? {}) as CoreNotificationPayload);
   };

Then __handleCoreNotificationForTests just delegates to handleCoreNotification. Note the helper would need to be module-scoped (or the listener can be extracted similarly to how the test helper exists).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/lib/nativeNotifications/service.ts` around lines 106 - 177,
coreNotificationListener and __handleCoreNotificationForTests duplicate the same
validation, truncation and dispatch logic; extract a single module-scoped helper
(e.g., handleCoreNotification) that accepts a CoreNotificationPayload, performs
the id/title checks, truncates title/body, maps deep_link to deepLink, and calls
dispatchAndMaybeBanner, then replace both coreNotificationListener and
__handleCoreNotificationForTests to delegate to that helper (ensure the
socketService.on registration still passes args[0] into the helper).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/openhuman/notifications/bus.rs`:
- Around line 73-131: The id generation in event_to_notification embeds now_ms()
making IDs change per publish; change to produce a stable logical id per event
kind instead: for DomainEvent::CronJobCompleted use format!("cron:{}:{}",
job_id, if *success {"ok"} else {"err"}) (no ts), for
DomainEvent::WebhookProcessed use the correlation_id (e.g. format!("webhook:{}",
correlation_id)) and if correlation_id is None fall back to
format!("webhook:{}", skill_id), and for DomainEvent::SubagentCompleted use
format!("subagent:{}:{}", parent_session, task_id) or at minimum
format!("subagent:{}", task_id); keep timestamp_ms = now_ms() as the event time
but remove ts from the id so frontend de-dup by stable ids.

---

Outside diff comments:
In `@src/core/jsonrpc.rs`:
- Around line 839-841: The event bus summary log currently printed by the
log::info! call that lists registered domain subscribers is missing the newly
added "notifications" subscriber; update the log message string used in that
log::info! invocation to include "notifications" alongside the existing items
(e.g., "...conversation, composio, restart, proactive, agent, notifications") so
the summary accurately reflects the notification bridge subscriber registered
elsewhere in the file.

In `@src/core/socketio.rs`:
- Around line 239-245: The doc comment for spawn_web_channel_bridge is out of
sync: it still says "four bridges" and the numbered list misorders the new
core_notification bridge (introducing 1,2,3,5,4). Update the comment to reflect
five bridges and make the numbering monotonic (e.g., renumber the transcription
bridge to 5 and the new core_notification block to 4, or reorder the blocks) so
the list reads 1–5 correctly and includes the new core_notification bridge.

---

Nitpick comments:
In `@app/src/lib/nativeNotifications/service.ts`:
- Around line 106-177: coreNotificationListener and
__handleCoreNotificationForTests duplicate the same validation, truncation and
dispatch logic; extract a single module-scoped helper (e.g.,
handleCoreNotification) that accepts a CoreNotificationPayload, performs the
id/title checks, truncates title/body, maps deep_link to deepLink, and calls
dispatchAndMaybeBanner, then replace both coreNotificationListener and
__handleCoreNotificationForTests to delegate to that helper (ensure the
socketService.on registration still passes args[0] into the helper).

In `@src/openhuman/notifications/bus.rs`:
- Around line 133-149: Add a grep-friendly trace/debug log inside
NotificationBridgeSubscriber::handle when event_to_notification(event) yields
Some(notification); specifically, log a short message including the DomainEvent
type/id or a succinct identifier and that it matched before calling
publish_core_notification, so you can trace matched events without digging into
publish_core_notification; update the handle method to call the tracer (trace!
or debug!) right after the if-let Some(...) match and include event and/or
notification identifying fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8b9c8b9-cd76-47c9-acec-5e8c0d8000e0

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc0d0d and fcb5e4a.

📒 Files selected for processing (7)
  • app/src/lib/nativeNotifications/__tests__/service.test.ts
  • app/src/lib/nativeNotifications/service.ts
  • src/core/jsonrpc.rs
  • src/core/socketio.rs
  • src/openhuman/notifications/bus.rs
  • src/openhuman/notifications/mod.rs
  • src/openhuman/notifications/types.rs

Comment on lines +73 to +131
pub fn event_to_notification(event: &DomainEvent) -> Option<CoreNotificationEvent> {
let ts = now_ms();
match event {
DomainEvent::CronJobCompleted { job_id, success } => Some(CoreNotificationEvent {
id: format!("cron:{}:{}", job_id, ts),
category: CoreNotificationCategory::Agents,
title: if *success {
"Cron job completed".into()
} else {
"Cron job failed".into()
},
body: format!("Job {job_id} finished."),
deep_link: Some("/settings/cron-jobs".into()),
timestamp_ms: ts,
}),
DomainEvent::WebhookProcessed {
skill_id,
status_code,
elapsed_ms,
error,
..
} => {
// Only surface failures — successful webhooks are noisy.
if error.is_none() && *status_code < 400 {
return None;
}
Some(CoreNotificationEvent {
id: format!("webhook:{}:{}", skill_id, ts),
category: CoreNotificationCategory::System,
title: "Webhook error".into(),
body: match error {
Some(err) => {
format!("{skill_id} webhook failed after {elapsed_ms}ms: {err}")
}
None => format!(
"{skill_id} webhook returned HTTP {status_code} after {elapsed_ms}ms"
),
},
deep_link: Some("/webhooks".into()),
timestamp_ms: ts,
})
}
DomainEvent::SubagentCompleted {
parent_session,
task_id,
agent_id,
output_chars,
..
} => Some(CoreNotificationEvent {
id: format!("subagent:{}:{}:{}", parent_session, task_id, ts),
category: CoreNotificationCategory::Agents,
title: "Sub-agent finished".into(),
body: format!("{agent_id} produced {output_chars} chars of output."),
deep_link: Some("/chat".into()),
timestamp_ms: ts,
}),
_ => None,
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stable-id semantics don't match the doc comment.

The id format is <kind>:<subject>:<ts> where ts is the current wall-clock milliseconds at translation time. Because ts is embedded, two publishes of the same logical event (e.g., a cron job retried, or the same webhook replayed) get different ids and therefore won't be de-duplicated on the frontend.

This contradicts the doc in src/openhuman/notifications/types.rs lines 25-27:

Stable id used for de-duplication in the center (e.g. "cron:<job_id>:<ts>"). The frontend keys by this id so repeated publishes for the same logical event don't pile up.

Two options:

  1. Keep the current behavior and fix the doc to say the id is unique per publish (not per logical event).
  2. Drop ts from the id and derive a stable id from the event's natural key (e.g. cron:<job_id> + success/failure, webhook:<correlation_id>, subagent:<task_id>). The frontend slice can then replace-by-id for true de-dup.

Given the PR comment says the frontend keys by id, option 2 is probably the intended behavior. correlation_id is already available on WebhookProcessed and task_id on SubagentCompleted — both are stronger stability guarantees than a timestamp suffix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/notifications/bus.rs` around lines 73 - 131, The id generation
in event_to_notification embeds now_ms() making IDs change per publish; change
to produce a stable logical id per event kind instead: for
DomainEvent::CronJobCompleted use format!("cron:{}:{}", job_id, if *success
{"ok"} else {"err"}) (no ts), for DomainEvent::WebhookProcessed use the
correlation_id (e.g. format!("webhook:{}", correlation_id)) and if
correlation_id is None fall back to format!("webhook:{}", skill_id), and for
DomainEvent::SubagentCompleted use format!("subagent:{}:{}", parent_session,
task_id) or at minimum format!("subagent:{}", task_id); keep timestamp_ms =
now_ms() as the event time but remove ts from the id so frontend de-dup by
stable ids.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring to follow-up — not blocking v1.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jwalin-shah, understood — happy to defer this to a follow-up. Would you like me to open a GitHub issue to track the stable-id de-duplication fix (dropping ts from the id and using natural event keys like cron:<job_id>:<ok|err>, webhook:<correlation_id>, subagent:<task_id>) so it doesn't get lost before Phase 2 polish?

(╯°□°)╯


🧠 Learnings used
Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-23T06:30:00.454Z
Learning: Applies to src/openhuman/*/bus.rs : Each domain owns its `bus.rs` file with `EventHandler` implementations; register subscribers at startup via the singleton API in the domain's startup or initialization code

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-23T06:30:00.454Z
Learning: Applies to src/core/event_bus/**/*.rs : Use `publish_global(event)` for fire-and-forget notifications (one publisher, many subscribers); use `request_native_global(method, req)` for typed one-to-one request/response with zero serialization

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T21:14:11.082Z
Learning: Applies to src/openhuman/*/bus.rs : Each domain should own a `bus.rs` with `EventHandler` impls. Convention: `<Purpose>Subscriber` + `name()` returning `"<domain>::<purpose>"`

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-22T21:14:11.082Z
Learning: New/changed behavior must ship with matching rustdoc / code comments; update AGENTS.md or architecture docs when rules or user-visible behavior change

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-23T06:30:00.454Z
Learning: Applies to src/openhuman/**/*.rs : When adding or changing domain behavior, ship matching documentation (rustdoc, code comments, or updates to `AGENTS.md` / architecture docs) before handoff

Learnt from: CR
Repo: tinyhumansai/openhuman PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-23T06:30:00.454Z
Learning: Applies to src/openhuman/**/*.rs : Implement features in this order: specify against the codebase → implement in Rust → JSON-RPC E2E tests → UI in the Tauri app → app unit tests → app E2E

Learnt from: sanil-23
Repo: tinyhumansai/openhuman PR: 416
File: src/openhuman/memory/relex.rs:441-464
Timestamp: 2026-04-07T15:49:51.275Z
Learning: When using the `ort` Rust crate v2.x with the `load-dynamic` feature enabled, don’t require individual execution-provider feature flags (e.g., `directml`, `coreml`, `cuda`) alongside `load-dynamic` to get EP registration code. The `ort` crate already compiles EP registration via `#[cfg(any(feature = "load-dynamic", feature = "<ep_name>"))]` guards, and adding per-EP feature flags can pull in static-linking dependencies that conflict with the dynamic loading approach. At runtime, EP availability is determined by what the dynamically loaded ONNX Runtime library (`onnxruntime.dll`/`.so`/`.dylib`) supports; ort docs indicate providers like `directml`/`xnnpack`/`coreml` are available in builds when the platform supports them.

Learnt from: sanil-23
Repo: tinyhumansai/openhuman PR: 416
File: src/openhuman/memory/relex.rs:441-464
Timestamp: 2026-04-07T15:49:51.275Z
Learning: When integrating the `ort` Rust crate v2.x with the `load-dynamic` feature enabled, do NOT also require/enable individual provider EP Cargo features like `directml`, `coreml`, or `cuda`. In `ort` v2.x, EP registration for providers (e.g., DirectML, CoreML, CUDA, etc.) is already compiled in under source-level `#[cfg(any(feature = "load-dynamic", feature = "<provider>"))]` guards, such as in `ep/directml.rs`. Adding provider feature flags alongside `load-dynamic` can pull in static-linking dependencies that conflict with the dynamic-loading approach. Provider availability should be treated as runtime-determined by what the loaded `onnxruntime` library (`onnxruntime.dll`/`libonnxruntime.so`/`libonnxruntime.dylib`) actually supports.

Learnt from: oxoxDev
Repo: tinyhumansai/openhuman PR: 571
File: src/openhuman/local_ai/service/whisper_engine.rs:69-80
Timestamp: 2026-04-14T19:59:04.826Z
Learning: When reviewing Rust code in this repo that uses the upstream `whisper-rs` crate (v0.16.0), do not report `WhisperContextParameters::use_gpu(...)` or `WhisperContextParameters::flash_attn(...)` as missing/invalid APIs. These builder-style methods exist upstream and return `&mut Self`; they are not limited to `WhisperVadContextParams`.

- Differentiate CronJobCompleted body for success vs failure paths
- Add SubagentFailed DomainEvent handler (was silently dropped)
- Add missing unit test for subagent_failed_produces_agents_notification
- Fix bridge numbering in socketio.rs: core_notification=4, transcription=5
  and update spawn_web_channel_bridge doc to say "five bridges"
- Use server timestamp_ms in native notifications service when available
- #[derive(Default)] on NotificationBridgeSubscriber; remove manual impl
- Add SAFETY comment near mem::forget in register_notification_bridge_subscriber
- Fix types.rs doc comment: id is unique-per-publish, not a stable dedup key
  (addresses @coderabbitai stable-id semantics mismatch)
@jwalin-shah
Copy link
Copy Markdown
Contributor Author

@senamakel — green + MERGEABLE, ready for a look when you have a sec.

@senamakel senamakel merged commit 7670ae9 into tinyhumansai:main Apr 23, 2026
7 checks passed
@jwalin-shah jwalin-shah deleted the feat/notification-bridge branch April 23, 2026 21:13
VectorJet pushed a commit to VectorJet/openhuman that referenced this pull request Apr 24, 2026
…umansai#395) (tinyhumansai#782)

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants