Skip to content

Architecture & Data Flow

Diagrams are written in Mermaid. They render automatically on GitHub and in VS Code (install the "Mermaid Preview" extension). You can also paste the code blocks into mermaid.live to view and export them.


Component Architecture

High-level view of how the major pieces fit together.

graph TB
    subgraph Users["Interfaces"]
        DiscordClient["Discord Client\n(user)"]
        TelegramClient["Telegram Client\n(user)"]
        AdminCLI["manage.py\n(admin CLI)"]
    end

    subgraph BotLayer["Bot Layer"]
        subgraph DiscordBot["Discord Bot  —  bot/"]
            ConvCog["conversation.py\non_message handler\nMOTD · onboarding · routing"]
            AdminCog["admin.py\n/profile · /reset · /link\n/set_platform · /set_timezone\n/delete_my_data"]
            SlashCogs["tasks.py · appointments.py\nslash command interfaces"]
        end
        subgraph TgBot["Telegram Bot  —  tg_bot/"]
            TgConv["handlers/conversation.py\nmessage handler\nMOTD · onboarding · routing"]
            TgCmds["handlers/commands.py\n/start · /link · /set_platform\n/profile · /delete_my_data"]
        end
        Notifier["notifier.py\nNotifier\nroutes per preferred_platform\n(discord · telegram · both)"]
    end

    subgraph AILayer["AI Layer  —  ai/"]
        Coach["coach.py\nbuild prompt → call Claude\nexecute tools → return text"]
        Memory["memory.py\nrolling history window\nsummarization at 60 msgs"]
        Tools["tools.py\ntool definitions +\nexecution handlers"]
        HabitsCatalog["habits_catalog.py\nin-code habit definitions\n(action + check-in types)"]
        subgraph Prompts["prompts/"]
            Base["system_base.py\nBASE_PROMPT (ADHD coaching)\nBASE_PROMPT_TASKONLY (neutral)\nHABIT_SETUP_MODE · triggers"]
            Context["task_context.py\ntask + appointment\nhabit + missed-task fragments"]
            Modes["coaching_modes.py\noverwhelm · focus\ncelebration · planning"]
        end
    end

    subgraph SchedLayer["Scheduler  —  scheduler/"]
        Daemon["daemon.py\nAPScheduler\nAsyncIOScheduler"]
        Reminders["reminders.py\ntask + appointment\nreminders"]
        ReminderQueue["reminder_queue.py\nfire-once push\nnotification queue"]
        Checkins["checkins.py\ndaily morning\ncheck-in"]
        HabitReview["habit_review.py\n60-day periodic\nhabit review"]
        WeeklySummary["weekly_summary.py\nFriday win summary"]
        WeeklyReview["weekly_review.py\nMonday weekly review"]
        Recurring["recurring.py\ngenerate daily task + appt instances\nmark missed · bump urgency"]
    end

    subgraph DataLayer["Data Layer  —  db/"]
        Repos["repos/\nuser · task · appt · message · habit\nrecurring_task · recurring_appt\noccasion · reminder · link_code"]
        Crypto["crypto.py\nFernet encryption\nat rest"]
        DB[("SQLite or PostgreSQL\n(DATABASE_URL)")]
    end

    subgraph WebLayer["Web Layer  —  web/"]
        WebServer["server.py\nFastAPI + uvicorn\nport 8000"]
        StaticSite["web/dist/\nMkDocs static site\n(baked into Docker image)"]
        HealthCheck["/health\nhealth check endpoint"]
    end

    subgraph External["External Services"]
        DiscordAPI["Discord API"]
        TelegramAPI["Telegram API"]
        AnthropicAPI["Anthropic API\nclaude-sonnet-4-6"]
        Browser["Browser\n(docs / landing page)"]
    end

    DiscordClient <-->|"WebSocket"| DiscordAPI
    TelegramClient <-->|"long polling"| TelegramAPI
    DiscordAPI --> ConvCog
    DiscordAPI --> AdminCog
    TelegramAPI --> TgConv
    TelegramAPI --> TgCmds
    AdminCog --> Repos
    TgCmds --> Repos
    AdminCLI -->|"direct DB access\nno bot needed"| Repos

    ConvCog --> Coach
    ConvCog --> Repos
    TgConv --> Coach
    TgConv --> Repos

    Coach --> Memory
    Coach --> Tools
    Coach --> Prompts
    Coach <-->|"messages.create()"| AnthropicAPI
    Memory --> Repos
    Tools --> Repos
    Tools --> HabitsCatalog

    Daemon --> Reminders
    Daemon --> ReminderQueue
    Daemon --> Checkins
    Daemon --> HabitReview
    Daemon --> WeeklySummary
    Daemon --> WeeklyReview
    Daemon --> Recurring
    Reminders --> Repos
    Reminders --> Notifier
    ReminderQueue --> Repos
    ReminderQueue --> Notifier
    Checkins --> Coach
    Checkins --> Notifier
    HabitReview --> Coach
    HabitReview --> Notifier
    WeeklySummary --> Coach
    WeeklySummary --> Notifier
    WeeklyReview --> Coach
    WeeklyReview --> Notifier
    Recurring --> Repos

    Notifier -->|"DM user"| DiscordAPI
    Notifier -->|"send_message()"| TelegramAPI

    Repos --> Crypto
    Crypto <--> DB

    WebServer --> StaticSite
    WebServer --> HealthCheck
    Browser -->|"HTTP :8000"| WebServer

Message Data Flow

Sequence diagram for a single user message through the system.

sequenceDiagram
    actor User
    participant Discord
    participant Conv as conversation.py
    participant Coach as coach.py
    participant Memory as memory.py
    participant Tools as tools.py
    participant Repos as db/repos
    participant Claude as Anthropic API

    User->>Discord: sends message / DM
    Discord->>Conv: on_message event

    Conv->>Repos: get_or_create_user()
    Repos-->>Conv: (user, is_new)

    alt First contact (is_new)
        Conv->>Discord: send MOTD
    end

    alt Onboarding not complete
        Conv->>Claude: onboarding system prompt + history
        Claude-->>Conv: next question or ONBOARDING_COMPLETE
        Conv->>Repos: save messages
        Conv->>Repos: update user profile (on completion)
        Conv->>Discord: send onboarding reply
    else Normal conversation
        Conv->>Coach: respond(user, message)

        Coach->>Repos: save_user_message()
        Coach->>Repos: list_active tasks
        Coach->>Repos: list_upcoming appointments
        Note over Coach: assemble system prompt:<br/>base (ADHD or task-only) + profile +<br/>tasks + appts + habit context +<br/>mode fragment (skipped if task-only)

        alt habits_setup_complete = False
            Note over Coach: inject HABIT_SETUP_MODE fragment<br/>Aria walks user through habit setup
        else Normal coaching mode
            Note over Coach: inject coaching_mode fragment<br/>(overwhelm / focus / celebration / planning)
        end

        Coach->>Memory: get_context_messages()
        Memory->>Repos: get_history(limit=20)
        Repos-->>Memory: recent messages + summary row
        Memory-->>Coach: formatted history

        loop Tool-use loop (max 5 rounds)
            Coach->>Claude: messages.create(system, tools, history)
            Claude-->>Coach: response (text and/or tool_use blocks)

            alt Response contains tool_use blocks
                loop Each tool call
                    Coach->>Tools: execute_tool(name, input)
                    Tools->>Repos: create / update / read
                    Repos-->>Tools: result
                    Tools-->>Coach: tool result string
                end
                Note over Coach: append assistant + tool_result turns<br/>to local message list, loop again
            else No tool calls — final response
                Coach-->>Conv: final text
            end
        end

        Coach->>Repos: save_assistant_message()
        Coach->>Memory: maybe_summarize()
        Note over Memory: if msg count ≥ 60, purge old rows,<br/>call Claude for summary, save summary row

        Conv->>Discord: send response (chunked if > 2000 chars)
        Discord-->>User: bot reply
    end

Proactive Message Flow

How the scheduler sends reminders, morning check-ins, and habit reviews without a user triggering anything.

sequenceDiagram
    participant APScheduler
    participant Recurring as recurring.py
    participant Reminders as reminders.py
    participant Checkins as checkins.py
    participant HabitReview as habit_review.py
    participant WeeklySummary as weekly_summary.py
    participant WeeklyReview as weekly_review.py
    participant Coach as coach.py
    participant Repos as db/repos
    participant Notifier as Notifier

    Note over APScheduler: recurring: hourly<br/>task/appt reminder: every 5 min / 1 min<br/>reminder queue: every 1 min<br/>morning check-in: every minute (match user time)<br/>habit review: daily at 10am UTC<br/>weekly summary/review: every minute (match user day+time)

    APScheduler->>Recurring: hourly instance generation
    Recurring->>Repos: list_active recurring tasks
    loop Each applicable recurring task
        Recurring->>Repos: mark previous-day open instances "missed"
        Recurring->>Repos: count_consecutive_missed → bump urgency (cap 85)
        Recurring->>Repos: create today's fresh Task instance
    end

    APScheduler->>Reminders: poll for due tasks
    Reminders->>Repos: list_due_for_reminder(now)
    Repos-->>Reminders: overdue tasks

    loop Each due task
        Reminders->>Notifier: send(user, reminder text)
        Reminders->>Repos: mark_reminder_sent(task_id)
    end

    APScheduler->>Reminders: poll for due appointments
    Reminders->>Repos: list_due_for_reminder(now)
    Repos-->>Reminders: appointments in reminder window

    loop Each due appointment
        Reminders->>Notifier: send(user, reminder text)
        Reminders->>Repos: mark_reminder_sent(appt_id)
    end

    APScheduler->>Checkins: fire morning check-in for user
    Checkins->>Repos: get user
    Checkins->>Coach: respond(user, CHECKIN_TRIGGER)
    Note over Coach: builds fresh context: occasion countdowns (lead window) +<br/>tasks + appts + recently-missed recurring tasks +<br/>today's recurring instances + habit check-in<br/>questions (active, paused, struggling)
    Coach-->>Checkins: check-in message text
    Checkins->>Notifier: send(user, check-in text)

    APScheduler->>HabitReview: daily habit review check
    HabitReview->>Repos: query users where habits_setup_complete=True<br/>AND last_habit_review_at < 60 days ago
    Repos-->>HabitReview: users due for review

    loop Each user due for review
        HabitReview->>Coach: respond(user, HABIT_REVIEW_TRIGGER)
        Note over Coach: builds context with habit stats +<br/>struggling habits, injects HABIT_REVIEW_INSTRUCTIONS
        Coach-->>HabitReview: review message text
        HabitReview->>Notifier: send(user, review text)
        Note over HabitReview: complete_habit_setup tool call<br/>resets last_habit_review_at
    end

    APScheduler->>WeeklySummary: every minute — check if any user's summary time matches now
    WeeklySummary->>Repos: get users due for weekly summary
    Repos-->>WeeklySummary: matching users

    loop Each user due for weekly summary
        WeeklySummary->>Coach: respond(user, WEEKLY_SUMMARY_TRIGGER)
        Coach-->>WeeklySummary: summary message text
        WeeklySummary->>Notifier: send(user, summary text)
    end

    APScheduler->>WeeklyReview: every minute — check if any user's review time matches now
    WeeklyReview->>Repos: get users due for weekly review
    Repos-->>WeeklyReview: matching users

    loop Each user due for weekly review
        WeeklyReview->>Coach: respond(user, WEEKLY_REVIEW_TRIGGER)
        Coach-->>WeeklyReview: review message text
        WeeklyReview->>Notifier: send(user, review text)
    end

Habit Tracking Data Flow

How check-in habit questions flow through the system during a morning check-in.

sequenceDiagram
    participant APScheduler
    participant Checkins as checkins.py
    participant Coach as coach.py
    participant Claude as Anthropic API
    participant HabitRepo as habit_repo
    participant Notifier as Notifier
    actor User

    APScheduler->>Checkins: fire check-in for user
    Checkins->>Coach: respond(user, CHECKIN_TRIGGER)

    Coach->>HabitRepo: get_pause() for each enabled habit
    Coach->>HabitRepo: get_struggling_habits() — ≥60% no in last 14 days
    Coach->>HabitRepo: get_stats() — yes/no ratios
    Note over Coach: format_habit_context() builds prompt fragment<br/>listing active/paused/struggling habits

    Coach->>Claude: check-in system prompt + habit context
    Claude-->>Coach: check-in message with habit questions

    Checkins->>Notifier: send(user, check-in + habit questions)
    Notifier-->>User: morning check-in (Discord or Telegram)

    User->>Notifier: "Yes, I woke up on time. No on screens."
    Notifier->>Coach: respond(user, reply)

    loop Each habit answer
        Claude->>Coach: tool_use: log_habit_checkin(habit_key, response)
        Coach->>HabitRepo: log_response(user_id, habit_key, True/False)
        Note over HabitRepo: upserts by (user_id, habit_key, date)<br/>one row per habit per day
    end

    alt Habit is struggling AND user said no
        Note over Claude: gently explores barrier<br/>(one open question, no judgment)
        User->>Notifier: explains barrier
        Claude->>Coach: tool_use: pause_habit_checkin(habit_key, pause_days, reason)
        Coach->>HabitRepo: set_pause() — encrypted reason stored
        Note over HabitRepo: habit skipped in check-ins until paused_until date
    end

Database Schema

erDiagram
    users {
        int id PK
        str discord_id "nullable; unique"
        str discord_username "nullable"
        str telegram_id "nullable; unique"
        str telegram_username "nullable"
        str display_name
        str timezone
        bool onboarding_complete
        str coaching_style "gentle|direct|cheerleader|none"
        str preferred_check_in_time "HH:MM local"
        int wins_count
        int focus_session_minutes "default 25"
        int checkin_max_items "tasks shown in morning check-in; default 5"
        str preferred_platform "discord|telegram|both"
        date vacation_until "nullable; suppresses proactive messages"
        str quiet_hours_start "nullable; HH:MM local"
        str quiet_hours_end "nullable; HH:MM local"
        int task_switch_buffer_minutes "nullable; injected into day-planning prompt"
        int break_interval_minutes "nullable"
        int break_duration_minutes "nullable"
        bool habits_setup_complete
        str habits_checkin_config "JSON; nullable"
        datetime last_habit_review_at "nullable"
        int weekly_summary_day "0=Mon…6=Sun; default 4 (Fri)"
        str weekly_summary_time "HH:MM local; default 16:00"
        int weekly_review_day "0=Mon…6=Sun; default 0 (Mon)"
        str weekly_review_time "HH:MM local; default 09:00"
        str last_seen_changelog "ISO datetime; nullable"
        str bot_persona "nullable; overrides Aria persona"
        bool is_test "synthetic accounts; excluded from broadcasts and stats"
        datetime created_at
    }

    tasks {
        int id PK
        int user_id FK
        str title "encrypted"
        str status "pending|in_progress|done|snoozed|missed|skipped|abandoned"
        str priority "low|normal|high|urgent"
        str source "manual|ai_suggested|calendar|recurring"
        datetime due_at
        datetime snoozed_until
        datetime reminder_sent_at
        int parent_task_id FK
        int estimated_minutes
        int recurring_task_id FK "nullable"
        date recurrence_date "nullable; set on recurring instances"
        str skip_reason "encrypted; set when status=skipped"
        int urgency_base
        int escalation_hours
        datetime completed_at
        datetime created_at
    }

    recurring_tasks {
        int id PK
        int user_id FK
        str title "encrypted"
        str recurrence_type "daily|weekdays|weekends|weekly|monthly_date|monthly_weekday|yearly|quarterly|semi_annual"
        str recurrence_days "weekday list or day-of-month"
        int recurrence_week_of_month "nullable; for monthly_weekday"
        int recurrence_month "nullable; anchor month for yearly/quarterly/semi_annual"
        int urgency_base
        int escalation_hours
        str default_reminder_time "nullable; HH:MM local"
        bool active
        datetime created_at
    }

    appointments {
        int id PK
        int user_id FK
        str title "encrypted"
        str description "encrypted; nullable"
        str location "encrypted; nullable"
        datetime starts_at
        datetime ends_at "nullable"
        int reminder_minutes_before
        int travel_minutes "nullable; added to reminder window"
        bool transition_reminder
        bool reminder_sent
        int recurring_appointment_id FK "nullable"
        date recurrence_date "nullable; set on recurring instances"
        str google_event_id "nullable; Phase 2"
        datetime created_at
    }

    recurring_appointments {
        int id PK
        int user_id FK
        str title "encrypted"
        str start_time "HH:MM local"
        int duration_minutes "nullable"
        str location "encrypted; nullable"
        int reminder_minutes_before
        bool transition_reminder
        int travel_minutes "nullable"
        str recurrence_type "daily|weekdays|weekends|weekly|monthly_date|monthly_weekday|yearly|quarterly|semi_annual"
        str recurrence_days "nullable"
        int recurrence_week_of_month "nullable"
        int recurrence_month "nullable; anchor month"
        bool active
        datetime created_at
    }

    occasions {
        int id PK
        int user_id FK
        str title "encrypted"
        str recurrence_type "none|yearly"
        date target_date "nullable; for one-shot occasions"
        int recurrence_month "nullable; 1-12 for yearly"
        str recurrence_days "nullable; day of month for yearly"
        int lead_days "days ahead to start countdown; default 7"
        datetime handled_at "nullable; silences countdown for this occurrence"
        str handled_note "encrypted; nullable"
        bool active
        datetime created_at
    }

    reminders {
        int id PK
        int user_id FK
        str message "encrypted"
        datetime fire_at "UTC datetime to send"
        datetime sent_at "nullable; set when delivered"
        bool active "false = cancelled"
        datetime created_at
    }

    messages {
        int id PK
        int user_id FK
        str role "user|assistant|tool"
        str content "encrypted"
        str tool_name "nullable"
        str tool_call_id "nullable"
        bool is_summary
        datetime created_at
    }

    platform_link_codes {
        int id PK
        int user_id FK
        str code "6-char; unique; 15-min TTL"
        datetime expires_at
        datetime created_at
    }

    habit_logs {
        int id PK
        int user_id FK
        str habit_key
        date date
        bool response
        datetime created_at
    }

    habit_pauses {
        int id PK
        int user_id FK
        str habit_key
        date paused_until
        str reason "encrypted"
    }

    api_call_logs {
        int id PK
        int user_id FK "nullable"
        str call_type "chat|checkin|habit_review|weekly_review|weekly_summary|onboarding|summarization"
        str model
        int input_tokens
        int output_tokens
        int latency_ms
        int tool_calls
        datetime created_at
    }

    audit_logs {
        int id PK
        int user_id FK
        str action "read|create|update|delete"
        str resource_type
        int resource_id
        str performed_by "user|ai|scheduler"
        datetime created_at
    }

    bug_reports {
        int id PK
        int user_id FK
        str platform "discord|telegram"
        str report_type "bug|feature_request"
        str description "encrypted"
        str context "encrypted JSON: messages+tasks+appts snapshot"
        str app_version
        datetime created_at
    }

    users ||--o{ tasks : owns
    users ||--o{ recurring_tasks : owns
    users ||--o{ appointments : owns
    users ||--o{ recurring_appointments : owns
    users ||--o{ occasions : owns
    users ||--o{ reminders : owns
    users ||--o{ messages : owns
    users ||--o{ platform_link_codes : owns
    users ||--o{ habit_logs : owns
    users ||--o{ habit_pauses : owns
    users ||--o{ api_call_logs : owns
    users ||--o{ audit_logs : owns
    users ||--o{ bug_reports : files
    tasks }o--o| recurring_tasks : "instance of"
    tasks }o--o| tasks : "subtask of"
    appointments }o--o| recurring_appointments : "instance of"