Skip to content

Repository files navigation

πŸˆβ€β¬› Baumy Brain

A private Telegram house-management secretary for one shared house β€” a memory-first group-chat gremlin that quietly remembers everything the house says, answers questions grounded in that memory, schedules reminders, learns the people who live there, and can forget things on request. It runs in the house group, reacts more than it talks, and never invents facts.

Baumy is not a personal assistant and not multi-tenant. It's one bot, one house, one shared memory β€” built so the group chat has to remember less, not scroll more.


What it does

🧠 Remembers the house Every message is captured, embedded, and distilled into a trust-gated knowledge graph. Ask "who's in the cave this week?" months later and it knows.
πŸ’¬ Answers, grounded Replies only from what the house actually said β€” with an honest miss when it doesn't know, never a hallucinated fact. Reacts (πŸ‘€πŸ§ πŸ‘) more than it speaks.
⏰ Reminders "remind us to pay rent friday" β†’ DST-correct, exactly-once delivery to the house group.
πŸ‘₯ Learns people People are first-class entities β€” housemates, guests, the landlord β€” with relationships, notes, and attributed sentiment (never a score, never volunteered).
πŸŒ™ Reflects A sleep-time job consolidates what it knows into durable per-person profiles β€” the "it learns over time" step.
🧽 Forgets on request "forget my full name" β†’ Baumy proposes exactly what it'll remove and only deletes on a confirm tap. Soft-hide (reversible) or hard-purge (right-to-be-forgotten).
πŸ” Admin dashboard Telegram magic-link login β†’ browse memory, manage reminders/tasks, grant access, tune the response policy.

The one idea that matters

Telegram privacy mode is off, so Baumy sees every group message β€” which means every message is untrusted, attacker-controlled input. The entire design follows one rule:

The LLM proposes; deterministic code disposes.

No path lets group text directly cause a privileged effect. A language model can suggest an action, but the thing that commits is always a human authorization β€” a confirm tap (deletes, reminders) or a live-authenticated dashboard session (grants, config) β€” or a code-resolved constant (the fixed send destination).

flowchart LR
    subgraph untrusted["🚫 untrusted zone β€” group text"]
        MSG["group message<br/>(anyone, anything)"]
        LLM["language model<br/>proposes"]
    end
    subgraph wall["🧱 the walls"]
        ORIGIN["origin/trust from<br/>Telegram-authed chat.id /<br/>from.id β€” never text"]
        TAP["confirm-tap wall<br/>callback_query"]
        DASH["dashboard authz<br/>live session, re-checked"]
        CODE["code-resolved<br/>constants only"]
    end
    subgraph privileged["βœ… privileged effects"]
        SEND["send to house<br/>(fixed chat id)"]
        GRANT["grants / config"]
        DELETE["delete memory"]
    end

    MSG --> ORIGIN --> LLM
    LLM -->|propose delete| TAP
    TAP -->|tap by authed member| DELETE
    LLM -.->|never picks recipient| CODE
    CODE --> SEND
    DASH -->|owner / admin session| GRANT

    classDef danger fill:#3a1414,stroke:#b04040,color:#fff
    classDef safe fill:#12341a,stroke:#3f8f52,color:#fff
    class MSG,LLM danger
    class SEND,GRANT,DELETE safe
Loading

Trust tiers are derived only from Telegram-authenticated fields, never message content:

flowchart TD
    IN["incoming content"] --> Q{"forwarded /<br/>bot origin?"}
    Q -->|yes| QUAR["πŸ”΄ quarantined<br/>never grounds a reply,<br/>never writes a fact"]
    Q -->|no| L{"which lane?<br/>chat.type + chat.id"}
    L -->|house group| UNTR["🟠 untrusted<br/>grounds replies,<br/>never privileged"]
    L -->|member DM| TRUST["🟒 trusted"]
    REFLECT["πŸ”΅ system<br/>Baumy's own reflections"] -.->|highest trust| GRAPH[("knowledge graph")]
    QUAR --> GRAPH
    UNTR --> GRAPH
    TRUST --> GRAPH
    GRAPH -.->|"supersede only if trust β‰₯ incumbent<br/>(memory-poisoning defense)"| GRAPH
Loading

System architecture

flowchart TB
    TG["πŸ“± Telegram<br/>house group + member DMs"]

    subgraph vercel["Vercel β€” Next.js 15"]
        WH["/api/telegram/webhook<br/>fast-ack: verify secret β†’<br/>enqueue β†’ 200"]
        DASH["πŸ” Admin dashboard<br/>magic-link + HMAC session"]
        INNGEST["/api/inngest<br/>function host"]
    end

    subgraph async["Inngest β€” all async / background work"]
        INGEST["ingest pipeline<br/>classify β†’ decide β†’ act"]
        REMIND["reminders<br/>arm Β· deliver Β· sweep"]
        REFLECT["reflection cron<br/>every 6h β†’ profiles"]
        TASKS["scheduled tasks<br/>+ proactive digests"]
        REEMBED["re-embed sweep"]
    end

    subgraph data["Neon Postgres + pgvector + pg_trgm"]
        MEM[("evidence + embeddings")]
        KG[("entities + facts")]
        OPS[("reminders Β· tasks Β· roster Β· config Β· audit")]
    end

    subgraph ai["models"]
        ANTH["Anthropic<br/>Haiku Β· Sonnet Β· Opus"]
        VOY["Voyage<br/>voyage-3.5-lite (512d)"]
    end

    TG -->|webhook| WH -->|event| INNGEST
    INNGEST --> INGEST & REMIND & REFLECT & TASKS & REEMBED
    INGEST --> MEM & KG
    INGEST -->|classify, reply, extract| ANTH
    INGEST -->|embed| VOY
    REFLECT --> KG
    REMIND --> OPS
    INGEST -->|send, fixed chat id| TG
    DASH --> MEM & KG & OPS
Loading

Why this shape: the webhook is fast-ack only (verify β†’ enqueue β†’ 200); every real decision happens downstream in Inngest, where retries, exactly-once claims, and idempotency live. Scope (house vs DM vs ignore) is resolved from stored config, never guessed.


The ingest pipeline

Every group message walks the same gauntlet. The write-gate (decide) is where the classifier's proposal becomes a code-disposed action, clamped by an origin↔action policy.

flowchart TD
    START["message event"] --> PRE["prefilter<br/>drop obvious noise (no LLM)"]
    PRE --> ORIGIN["resolve origin + trust<br/>(authenticated fields only)"]
    ORIGIN --> CMD{"member-DM<br/>command?"}
    CMD -->|"/start /dashboard /pause"| DETERM["deterministic handler"]
    CMD -->|no| CLASSIFY["classify (Haiku)<br/>intent Β· confidence Β· respond Β· tier"]
    CLASSIFY --> DECIDE{"decide()<br/>write-gate + policy"}

    DECIDE -->|worth remembering| CAP["πŸ“₯ capture<br/>(orthogonal to the action)"]
    DECIDE -->|reminder| REM["⏰ auto-commit reminder"]
    DECIDE -->|forget| FGT["🧽 propose deletion<br/>β†’ confirm tap"]
    DECIDE -->|question / directed| RPLY["πŸ’¬ grounded reply"]
    DECIDE -->|else| REACT["😼 react or stay silent"]

    CAP --> EXTRACT["extract facts<br/>(paginated, no cap) β†’<br/>reconcile into graph"]
    RPLY --> RETRIEVE["hybrid retrieval β†’<br/>Sonnet, self-escalates to Opus"]

    classDef act fill:#14243a,stroke:#4070b0,color:#fff
    class CAP,REM,FGT,RPLY,REACT act
Loading

Capture is orthogonal to the action: a message can set a reminder and state a durable fact ("Zuzana arrives 10pm, staying in my room") β€” both are kept. Every structured-output call on this path is best-effort β€” a malformed model response degrades to a safe default instead of crash-looping the pipeline.


The memory pipeline β€” the crown jewel

flowchart LR
    MSG["message"]
    Q["question"]

    subgraph capture["1 Β· capture"]
        EV["evidence item<br/>+ Voyage embedding"]
        DEDUP["consolidate<br/>β‰₯0.97 cosine β†’ bump"]
    end
    subgraph facts["2 Β· facts"]
        EX["extract triples<br/>(Sonnet, paginated)"]
        RES["resolve entity<br/>normalize β†’ alias β†’<br/>trigram merge"]
        RECON["reconcile<br/>trust-gated Β· bitemporal"]
    end
    subgraph retrieve["3 Β· retrieve"]
        SEM["semantic<br/>(pgvector cosine)"]
        LEX["lexical<br/>(tsvector FTS)"]
        RRF["RRF fusion<br/>+ recency + salience"]
        DEEP["deep tier:<br/>expansion/HyDE + rerank"]
    end
    subgraph reflect["4 Β· reflect (sleep-time)"]
        PROF["per-person profiles<br/>β†’ system-trust facts"]
    end

    MSG --> EV --> DEDUP --> STORE[("evidence + vectors")]
    MSG --> EX --> RES --> RECON --> GRAPH[("entities + facts")]
    Q --> SEM & LEX
    STORE --> SEM
    SEM & LEX --> RRF --> DEEP --> ANSWER["grounded answer"]
    GRAPH --> ANSWER
    GRAPH --> PROF --> GRAPH
Loading
  • Capture stores the raw message + embedding; a near-verbatim restatement consolidates onto the original (salience bump) instead of duplicating.
  • Facts distils {subject, predicate, object} triples into a trust-gated, bitemporal graph. People are first-class; entity resolution de-fragments surface forms ("the sink" / "kitchen sink" β†’ one node) while keeping distinct people distinct.
  • Retrieve fuses semantic βŠ• lexical recall via Reciprocal Rank Fusion, composes recency + salience, and on the deep tier adds query expansion/HyDE and a re-rank.
  • Reflect runs on a slow cron: it re-reads a person's own facts + notes and writes a durable profile back as a system-trust fact β€” from non-secret, non-quarantined material (already-captured evidence + facts, not live group text; secrets and forwarded/bot content are excluded).
  • Nothing is ever deleted automatically. Salience is a ranking signal (de-noise), never a delete policy β€” the bitemporal graph is append-only.

Deletion on request β€” proposes, then a tap disposes

The one place memory does get removed is an explicit human request, and it still goes through the confirm wall:

sequenceDiagram
    participant M as Member
    participant B as Baumy ingest
    participant DB as Postgres
    participant C as Callback handler

    M->>B: forget my full name
    B->>B: classify, intent = forget
    B->>B: extract target (Sonnet, speaker-aware)
    B->>DB: findMemoryToForget, group-scoped
    DB-->>B: exact matching rows
    B->>M: I'll forget these 2 things… [Forget] [Keep]
    M->>C: taps Forget (authenticated from.id)
    C->>DB: soft-hide OR purge (redact + drop vector)
    C->>DB: write audit log
    C-->>M: 🧽 Forgotten β€” 2 things gone.
Loading

Soft hides rows from all recall (reversible, audited); purge redacts the stored value and drops the embedding (right-to-be-forgotten). The mode is chosen from the request's wording and shown in the proposal before anything commits. Reminders are the one exception to the wall β€” they auto-commit, because a reminder only posts text to the fixed house group.


Model routing

Anthropic only for language models (Voyage is the one deliberate exception, for embeddings β€” Anthropic ships none). Each role is pinned and env-overridable; ids are never inlined.

flowchart LR
    C["🟒 Haiku<br/>routing / triage ONLY"] --> D{"needs an answer?"}
    D -->|reply, memory ops| S["🟑 Sonnet<br/>primary reasoning"]
    S -->|"reply path<br/>self-escalates"| O["πŸ”΄ Opus<br/>advisor / deliberative"]
Loading

Cheap high-volume triage is Haiku; everything that reasons β€” replies, fact extraction, query expansion, re-rank, reflection, forget β€” runs on Sonnet. The reply path additionally self-escalates to Opus when it signals it needs more; the other Sonnet ops are fixed-tier (Opus is also the deliberative tier for scheduled tasks).


Stack

  • Next.js 15 (App Router) + React 19 + TypeScript, flat root layout (@/* β†’ ./)
  • Drizzle ORM + Neon Postgres + pgvector + pg_trgm
  • Inngest for all async / background / scheduled work
  • Anthropic (@ai-sdk/* + ai SDK) for language models Β· Voyage voyage-3.5-lite (512-dim) for embeddings
  • Auth: Telegram magic-link β†’ signed HMAC session cookie
  • Secrets at rest: AES-256-GCM (wifi/door/bank values); only a non-secret descriptor is stored + embedded
  • Tests: Vitest + PGlite (offline, in-memory Postgres with pgvector + pg_trgm) plus a real pgvector Postgres e2e via testcontainers

Commands

pnpm dev            # next dev
pnpm inngest:dev    # local Inngest dev server
pnpm typecheck      # tsc --noEmit
pnpm test           # vitest run (offline; PGlite)
pnpm build          # next build
pnpm db:generate    # drizzle-kit generate (migrations)
pnpm db:migrate     # apply migrations (needs DATABASE_URL_UNPOOLED)
node --experimental-strip-types scripts/set-webhook.ts   # register the Telegram webhook

See SETUP.md for a full local + deploy walkthrough, and AGENTS.md for the working guide + security invariants. Detailed design specs live in docs/spec/.


Built memory-first: the whole point is recall you can trust. Baumy remembers so the house doesn't have to β€” and only forgets when you ask it to. 😼

About

A Inngest backed secretary/sentient pet cat for managing shared house telegram chats.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages