Notification / docs-site/
v0.3.5 VOPI build

Notification system — proactive breakout from reply-only

Before criterion #6 the agent could only respond when B spoke. It had no tool to break out and notify on its own. Now it does.

Three channels

notification.py:deliver(text, mode, ...) dispatches via one of five modes:

ModeWhat happensWhen to use
voicePush text into Gemini via bridge._gemini.send_text() so the model speaks it in the next turnVoice session is active; B is in the channel
dmSend a Discord DM directly to the user via the bot adapterB is offline / AFK
channelPost to a text channel in the same guildA "voice log" or notes channel exists
webhookEmit a agent.notify event class webhookConfigure a webhook URL in the channel where you want the brief to land
autoTry voice → dm → channel → webhook in order, return first successDefault. The right choice when you don't know B's state.
allFire all four at onceFor the "definitely get this to B" case

Gemini tools

local_notify

{
  "text": "Hey, codex is back online",
  "mode": "auto",
  "title": "Tool online",
  "source": "health_watcher"
}

Returns:

{
  "status": "ok",
  "channel": "voice",
  "results": {"voice": {"status": "ok", "queued": true}, ...},
  "notified_at": 1749312456.7
}

local_notify_schedule

{
  "text": "Reminder: standup in 10",
  "delay_seconds": 600,
  "mode": "auto"
}

Returns {scheduled: <id>, fire_at_epoch: <ts>}. The schedule persists to ~/.hermes/voice-users/notifications-schedule.jsonl and survives bridge restarts. Polled every 2s by a background thread.

Other actions on the same tool:

  • {"list": true} → list all scheduled
  • {"cancel_id": "abc123"} → remove one

Sidecar HTTP endpoint

For callers that aren't in a Gemini session (e.g. cron jobs, subagents):

curl -X POST -H "Content-Type: application/json" \
     -d '{"text":"inbox is huge","mode":"dm","title":"Brief"}' \
     "http://127.0.0.1:18943/notify"

Also accepts GET with ?text=...&mode=...&title=... for trivial cases. The handler reads BRIDGE from the global to find the live GeminiLiveBridge for the voice mode; falls through to other modes if voice is unavailable.

AFK ping from opencode watcher

The opencode watcher (which polls long-running local_delegate_execute tmux sessions) was extended to call notification.deliver(mode="auto") after a session finishes. So when B is AFK and a 5-minute refactor finishes, the bot DMs B "refactor done" automatically.

Webhook fanout

The notification dispatcher also fires the agent.notify event class via WebhookDispatcher, which is configurable through env vars:

DISCORD_VOICE_LIVE_WEBHOOK_AGENT_NOTIFY=https://discord.com/api/webhooks/...

If unset, the dispatcher still tries DM and channel paths. Setting the webhook URL just gives you a third destination.

Notification sfx

When a notification is delivered, the bridge plays the notification sfx slot (see sfx-library.md). To disable: DISCORD_VOICE_LIVE_SFX_ENABLED=false.

When NOT to use

  • For task results (delegate finished, web search returned), prefer the tool's own return value to the model — the model is already in the loop and will narrate.
  • For urgent alerts that must interrupt B (call coming in, server down), use the webhook directly with urgent: true to bypass throttling.
  • For scheduled reminders the user asked for, use local_notify_schedule — it has a clear audit trail.