Skip to content

Notifications framework v1.5 followups (rolling) #99

@kaghni

Description

@kaghni

Context

Tracks v1.5 work on the notification framework shipped in:

  • hivemind#96 — feat/notifications-framework (centralized framework + Claude Code adapter, MERGED)
  • deeplake-api#209 — feat/notifications-endpoint (GET /me/notifications + dismiss, MERGED)

Notifications are now live in prod — confirmed working with the welcome banner appearing in real Claude Code sessions on org may4. This issue is the rolling tracker for what we build on top of that channel: server-emitted content that justifies hivemind's value to the user every time they open an agent.

The notification banner is just a delivery surface. The real v1.5 work is what we send through it.

v1.5 priorities

[v1.5] Onboarding recap on session start (the heavy work)

Goal: every session start, the user sees a short, concrete reminder of why hivemind is paying off — token savings, memory hits, and (eventually) org-level activity. Today they get a generic welcome and whatever an operator manually inserted into the DB. We want continuous, personalized, automatic reinforcement of the plugin's benefits.

This rolls up three line items that share the same pipeline (server-side cron → INSERT into notifications → existing hivemind delivery channel renders the banner). Single branch: feat/onboarding-notifications.

  • Proactive value recap on session start. Short summary of what hivemind has done for this user since their last session: top retrieved memories, top auto-pulled skills, sessions where memory injected useful context. Cron-driven, dedup'd per ISO-week. Triggered to feel "earned" rather than spammy — only fires when there's actually something to report.
  • Token savings tally. Estimate tokens saved by hivemind context injection vs the counterfactual (raw user prompt with no memory). Cumulative for the org + delta since last recap. Surfaced as info severity. Even rough numbers ("~12k tokens saved this week, ~$0.18 at Sonnet rates") move the needle on perceived value.
  • Org analytics digest. Multi-user activity summary for org admins (or all members, TBD): who's most active, top skills used across the team, memory write-vs-read ratio. Weekly cadence; only org-wide notifications (target_org_id) so a single row fans out to all members.

Shared design:

  • Trigger: server-side cron in deeplake-api, daily scan, INSERTs rows into notifications with target_user_id (recap, token savings) or target_org_id (org analytics).
  • Dedup: dedup_key keyed on <rule>-<user_or_org_id>-<ISO-week> so rerunning the cron is idempotent. Requires the notifications_dedup_key_uniq index from the auto-balance item below — they share infra.
  • Expiry: expires_at = now() + interval '7 days' so a stale recap drops out if the user doesn't open an agent for a while.
  • Content: title ≤ 80 chars, body ≤ 280 chars (well below the schema cap; keep it glanceable). Title/body composed from the org's actual data — empty/zero rollups skip the row entirely.
  • Source data:
    • Recap / token savings: memory query log + skill auto-pull log already in deeplake-api Postgres. New aggregation queries; no new tables.
    • Org analytics: organization_members join + activity rollup. May need a new org_activity_rollups materialized view for weekly cadence (decide during implementation).

Out of scope for v1.5:

  • Real-time push (we still poll at session start; "real-time" is a v2 concern).
  • Per-rule user opt-out (covered by global ~/.deeplake/notifications-state.json dedup; per-rule mute is a later refinement).
  • Email / Slack / web UI delivery — targets TEXT[] already supports it but no consumer ships v1.5.

[v1.5] Auto-emit "low balance" notifications via server-side cron

Problem: Today, "low balance" reminders only appear if an operator manually INSERTs a row. We want the server to automatically scan billing accounts daily and surface a notification per affected user.

Why this is server-side cron, not a client rule:

  • Audit trail in DB (we know exactly which users were warned, when, dedup'd how)
  • Single source of truth for thresholds (changing $20 → $30 doesn't require shipping a new hivemind version)
  • Works for users who don't use hivemind regularly (web UI / API consumers)

Design:

  1. New cron job in deeplake-api:
    • Schedule: daily at 12:00 UTC (or whatever ops prefers)
    • Mechanism: external scheduler (k8s CronJob, AWS EventBridge, or pg_cron if we prefer DB-side) hitting an internal endpoint OR a Go scheduled task in cmd/worker
  2. Job logic:
WITH eligible AS (
  SELECT
    om.user_id,
    o.id AS org_id,
    ba.balance_cents,
    ba.tier_v2
  FROM billing_accounts ba
  JOIN organizations o ON o.id = ba.org_id
  JOIN organization_members om ON om.org_id = o.id
  WHERE ba.balance_cents < 2000  -- < $20 USD
    AND ba.status = 'active'
)
INSERT INTO notifications
  (target_user_id, severity, title, body, dedup_key, expires_at)
SELECT
  user_id, 'warn', 'Balance running low',
  format('Your org has $%s remaining. Top up at deeplake.ai/billing.', balance_cents::float / 100),
  format('low-balance-%s-%s', org_id, to_char(now(), 'YYYY-WW')),  -- one per org per ISO-week
  now() + interval '7 days'
FROM eligible
ON CONFLICT (dedup_key) DO NOTHING;  -- requires UNIQUE INDEX on dedup_key
  1. Schema change required (shared with onboarding recap above):
-- Migration 23: enforce dedup_key uniqueness for cron idempotency
CREATE UNIQUE INDEX notifications_dedup_key_uniq ON notifications (dedup_key)
  WHERE dedup_key != '';

⚠️ Existing rows have dedup_key = '' default. Either backfill before the unique index, or keep it conditional on dedup_key != '' as above.

  1. Tier 4 follow-on tests:
    • Run cron with mocked balance data → expected rows inserted
    • Run twice in same week → second run is no-op (ON CONFLICT)
    • Bump balance above threshold mid-week → next session's GET filters out (expired in 7 days OR you can DELETE proactively)

Open questions for the implementer:

  • Where does the cron live? cmd/worker (Go), cmd/server startup goroutine, or external (k8s CronJob hitting an admin endpoint)?
  • What thresholds and cadence? $20 < weekly; <$5 < daily?
  • Severity escalation? info for "approaching", warn for "low", error for "exhausted"?

Estimated work: ~2 days. Migration + cron implementation + integration tests + ops runbook entry.

[v1.5] Admin tooling — POST /admin/notifications + CLI

Load-bearing for both items above, since the cron job IS an admin write. Build the endpoint and CLI alongside the cron so ops has a manual-fire path too.

  • POST /admin/notifications endpoint on deeplake-api
    • Body shape: { target: { type: "all_users" | "user_ids" | "org_ids" | "filter", ... }, severity, title, body, dedup_key, expires_at }
    • Returns: { fanout_count, preview_user_ids: [...3 sample] }
    • Auth: admin-scoped JWT or separate API token type
    • Audit log table: notification_admin_log(actor_id, target_spec, dedup_key, fanout_count, created_at)
    • Reject duplicates by dedup_key (idempotency — uses the same UNIQUE INDEX as auto-balance)
    • --dry-run mode that reports fanout count + sample IDs without inserting
  • hivemind admin notify CLI subcommand on the npm package (wraps the endpoint above)
  • Admin UI form at deeplake.ai/admin/notifications (deferred to v2 unless ops needs it sooner)

Content conventions (operator guidance)

Documented for operators sending notifications via SQL or admin tooling. See also deeplake-api/docs/notifications-handbook.md.

  • Avoid ZWJ emoji clusters (👨‍👩‍👧‍👦, 🏳️‍🌈) in body content — terminal renderers collapse adjacent ZWJ sequences.
  • Avoid starting titles with the same emoji as the severity prefix (⚠️ Low balance becomes ⚠️ ⚠️ Low balance because the severity prefix is also ⚠️).
  • Single-codepoint emoji (🐝 ⚠️ 🚀 🚨 ✅) work universally.
  • Keep title ≤ 80 chars, body ≤ 280 chars for the recap/digest content (the schema cap of 4000 is a hard guard, not a UX guideline).

Won't fix in v1.5

Items previously tracked here that aren't worth the engineering. Preserved for audit trail; do not reopen without strong evidence that real users are hitting them.

  • State file race condition under concurrent sessions — duplicate banner across two simultaneously-opened terminals only; race window is ~ms; users open sessions sequentially. Not a real-world bug.
  • Body length cap UX risk — content guidance + ≤280 char convention above handles it. Schema cap stays at 4000.
  • Per-channel delivery tracking (intent vs delivery split) — only matters when a real second consumer (web UI, email, slack) ships and needs per-(user, target) state. Not load-bearing.
  • Per-user notification dismissals — folded into the deliveries-table work above. Same reason: nothing actually calls POST /dismiss from the client today.
  • Per-agent expansion (Codex / Cursor / Hermes / Pi notification adapters) — orthogonal to value delivery. Add agents when there's a user asking for them, not preemptively.
  • Phase B / C client-driven local rules — superseded by server-side cron design (more durable, simpler, single source of truth).

How to update this issue

When new findings emerge during implementation of the items above, append to the relevant section. We keep one rolling source of truth for v1.5 notification work — do NOT fan out to many small issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions