Notifications

How Forven routes alerts — in-app vs. Discord, dedupe-by-key with cooldown, severity-aware gating, and per-event delivery preferences.

Forven raises a notification whenever something happens that you might want to know about — an approval is waiting, a trade opened, a data stream went stale, a job errored. This page explains how those alerts are routed, why a noisy condition does not spam you, why a critical alert always reaches you anyway, and how you set per-event delivery preferences. It is written for the operator who wants the system to be loud about the right things and quiet about the rest.

Every alert in Forven flows through a single path — emit_notification() — so the routing rules below apply uniformly no matter which subsystem raised the alert. You read and clear notifications in the in-app notification center and on the /ops console; you configure delivery in the Notifications section of Settings.

The one path: emit_notification()

There is exactly one way a notification enters the system. The scheduler, the brain workers, the health monitor, the trading layer, and the bot all call the same emit_notification() function. That call carries a small, structured record:

  • event_type — what happened, e.g. approval_required, trade_opened, system_degraded. This is the key the routing policy keys off.
  • severity — how important it is. critical severity is treated specially (see Severity-aware dedupe).
  • source — which subsystem raised it.
  • title, summary, body — the human-readable content, from a one-line headline to the full detail.

Because everything funnels through one function, routing, deduplication, and the delivery audit log are applied consistently. There is no side channel that bypasses the policy.

Channels and routing

When emit_notification() fires, resolve_notification_policy() reads your merged delivery preferences and resolves the alert to exactly one routing decision:

RoutingWhat it does
app_onlyShown in the in-app notification center only. Never leaves your machine.
discord_immediatePushed straight to Discord as soon as it fires.
discord_threadDelivered into a Discord thread, to keep related alerts grouped.
dropSilenced — recorded but not surfaced.

The default posture is conservative: most events resolve to app_only. You opt specific event types into Discord rather than opting the noisy majority out. The in-app center is always the system of record — even a drop is written down; it just is not pushed anywhere.

Discord delivery is optional and off unless you have wired it up. It requires the Forven Discord bot to be running (DISCORD_TOKEN set and START_BOT=1). With no Discord configured, every alert still lands in the in-app center — you simply lose the push channel.

Dedupe by key, with a cooldown

A flapping condition — a data stream that wavers either side of its SLA, a job that retries — could fire the same alert dozens of times a minute. Forven suppresses that. Notifications are deduplicated by a dedupe key with a cooldown: once an alert with a given key has been delivered, matching alerts within the cooldown window are collapsed rather than re-pushed.

This is what makes the system safe to leave running unattended. You get told that a condition exists; you do not get told a hundred times.

Severity-aware dedupe

Deduplication has one deliberate exception, and it is the most important rule on this page:

A critical alert is never suppressed by dedupe against a matching non-critical row. Even if an alert with the same dedupe key was recently delivered, a critical-severity event ignores that cooldown and is delivered anyway.

The reasoning is plain: dedupe exists to spare you noise, not to hide an emergency. If a low-severity version of a condition was already collapsed and the condition then escalates to critical, the critical alert breaks through. Operators always get the critical signal, even when everything around it is being quieted.

The delivery audit log

Every routing decision is recorded. Forven keeps a delivery audit log of what was emitted, how it was routed, and whether it was deduped or delivered. This is the transparency-over-confidence stance applied to alerting: you can reconstruct after the fact why you did — or did not — get pinged about something, rather than wondering whether an alert was lost.

The notifications table is pruned on a retention window by the maintenance job — an illustrative 60 days by default. Older delivery records are aged out, so if you are auditing something weeks old, do it before it falls out of the window or keep a backup. See Database & maintenance for the retention windows and how pruning works.

Acting on a notification

A notification is not just a message — you can act on it directly from the in-app center or /ops:

  • Acknowledge — mark it handled. POST /api/notifications/{id}/acknowledge.
  • Resend — re-route it through the current policy, useful after you have changed a preference. POST /api/notifications/{id}/resend.
  • Create a repair task — hand the fault to an agent to investigate and fix. POST /api/notifications/{id}/repair with a body of {"agent_id": "full-stack-engineer"}.

The repair action creates an agent_task (type notification_repair) carrying the notification's payload and metadata. The agent picks it up, investigates, applies a fix where it can, and summarizes the outcome in the task output. You can then watch it in the task queue. Repair tasks are a Forged-tier capability — they invoke the agent layer.

# Acknowledge a notification (operator-gated if FORVEN_OPERATOR_KEY is set)
Invoke-RestMethod -Method Post `
  -Uri "http://127.0.0.1:8003/api/notifications/123/acknowledge"

# Re-route it through the current policy
Invoke-RestMethod -Method Post `
  -Uri "http://127.0.0.1:8003/api/notifications/123/resend"

# Hand it to an agent as a repair task
Invoke-RestMethod -Method Post `
  -Uri "http://127.0.0.1:8003/api/notifications/123/repair" `
  -ContentType "application/json" `
  -Body '{"agent_id": "full-stack-engineer"}'

Configuring delivery preferences

You decide which event types are loud. Per-event delivery preferences live in the Notifications section of Settings, and each event type can be sent in-app only or pushed to Discord. Saving writes a flat set of per-event flags to the backend, which update_notification_preferences() persists to the forven:notification_preferences key in the config key-value store. From then on, every emit_notification() call resolves against your merged preferences.

Steps: route an event to Discord

  1. Open /settings and go to the Notifications section (anchor #notifications).
  2. Find the event type you want to change — for example approval_required, trade_opened, or system_degraded.
  3. Toggle its Discord channel on (or off to keep it in-app only).
  4. Save. Settings tracks the change as a pending edit until you save it.
  5. Confirm Discord is actually reachable — the bot must be running (DISCORD_TOKEN, START_BOT=1) for a push to land.

The save issues a request shaped like this:

PUT /api/settings/notification_preferences
{
  "approval_required_to_discord": true,
  "trade_opened_to_discord": false,
  "system_degraded_to_discord": true
}

What you'll see

After saving, the next time a matching event fires it is routed under the new policy: an event you switched to Discord arrives in your channel (subject to dedupe), while in-app delivery to the notification center continues regardless. Existing notifications are not retroactively re-routed — if you want to push one that already fired through the new policy, use Resend on that notification. Critical alerts continue to break through dedupe no matter what you configure.

How health alerts use this

The health monitor is one of the loudest callers of emit_notification(). It tracks component states (green / amber / red) and per-stream data freshness, and when a component or stream degrades past warning severity it routes the alert through the same policy described here. So your Notifications preferences govern whether a stale data stream or an amber component pings Discord or stays in-app — there is no separate health-alert channel to configure.

Caveats

  • Discord is opt-in and requires the bot. With no DISCORD_TOKEN / START_BOT=1, every alert still lands in-app, but nothing is pushed. A "missing" Discord alert is usually a bot that is not running.
  • Critical alerts ignore your dedupe — by design. You cannot tune a critical event into silence via the cooldown; that is the intended safety behaviour, not a bug.
  • The audit log is not forever. Notifications prune on a retention window (an illustrative 60 days). Audit old delivery records before they age out, or keep a backup.
  • Resend uses the current policy. Re-routing an old notification applies whatever preferences are set now, not the policy that was active when it first fired.
  • Repair tasks need the agent layer. POST /api/notifications/{id}/repair is a Forged-tier action; it has no effect without agents available.

Forven is a research tool. Notifications report on a research process — they are not predictions, recommendations, or financial advice, and no alert setting creates an edge or guards against loss.