VIEN
Back to the devlog

Devlog

How this project was built and deployed, step by step. It includes the parts that went wrong and were fixed and the difficulties of running it for real on a Raspberry Pi 5, because that is where most of the learning was.

D-01

Version 1: a few small bots

It started with a simple idea
make the chat less empty

  1. Split out of the music bot

    The companions were born inside the music bot's repo, because the idea came up there: a server with a music bot can still feel empty between songs. A few days later they moved to their own project. They share nothing at runtime, someone who only wants lively chat should not have to download a music bot, and the two repos can be updated or rolled back separately. On the Raspberry Pi they are just two folders and two containers.

  2. Manners before jokes

    One channel only, quiet hours, a daily limit, and stepping back when people are talking. The bots do not read message content: knowing that someone wrote something is enough.

  3. Bots stay bots

    They keep the BOT tag, never pretend to be human and never claim to eat, sleep or travel. Making a server feel alive is fine; pretending to be people is not.

D-02

Version 2: they can play

Ask, quiz, vote
and see who joins in

  1. Question of the day, polls, trivia

    A question at a fixed hour, real Discord polls, trivia with answer buttons and a weekly board. Each server can add its own questions, jokes and facts with a command.

  2. Measuring whether it helps

    /companions stats shows how many conversations actually got people talking. If the bots only talk to themselves, you should know.

  3. Timing is the hard part, not the jokes

    The first tests used time windows that were too short, so a punchline test passed for the wrong reason. Moving the clock in small steps and asserting who speaks when (and who does not) was far more honest. Another bug: a person's message in the exact millisecond a conversation began was not counted as joining in; comparing with >= fixed it.

D-03

Version 3: sitting with people

Present 24/7
even when everyone has left

  1. Voice is a reconcile loop

    Reacting to every disconnect event gets messy (Discord moves bots between voice servers, kicks, restarts). Instead every 15 seconds a bot compares what should be true (saved room, number of bots) with what is true and repairs it, waiting longer after failed joins. Restarting the container restores the room; I checked that on the Pi.

  2. Several rooms, sticky bots

    Each room remembers its own bots: new or bigger rooms only use free ones. At first I re-dealt everything whenever a room changed and bots were pulled out of other rooms. The complaint was fair.

  3. The "+ New Channel" loop

    A create-a-room channel made each bot spawn its own room and keep rejoining the original. Now the bots learn the room that was created, share it and no longer go back to the creator channel.

  4. Tools around the room

    A greeting when someone joins, Pomodoro, a voice time board, a welcome for new members (reading the system message type instead of asking for a special intent), reminders and event countdowns. Anything holding user IDs got its own erase command and a line in the "What is stored" table.

D-04

Version 4: safe to leave alone

Nobody watching
and it still has to be reliable

  1. A tick must never overlap itself

    With many servers and slow voice joins one round could outlast the 15 second timer and a second one would start on top of it, joining the same bot twice. Each loop now skips a round while the last one still runs.

  2. Learning from usage, carefully

    Adaptive tuning multiplies the default weights by how well each kind engages in that server, bounded to half and double, pulled toward the average while data is thin, and off until 20 conversations exist. Because it can be explained in one sentence, it can be reported in /companions stats.

  3. A measurement instead of a guess

    300 simulated servers for 8 hours: the slowest tick took about 30 ms, memory grew a few tens of MB, every server stayed inside its daily cap.

  4. Alerts and backups

    An optional webhook tells you when a bot loses its login or cannot rejoin voice, and the data files are backed up daily with 7 copies kept. Per-server data export and delete, and an automatic clean-up a day after the last bot leaves a server.

  5. Secrets in tests

    A test that wanted a token-shaped string was blocked by GitHub push protection. The string is now built from pieces; the warning was not bypassed.

D-05

Version 4.x: thirty personalities

From nine to thirty
each bot with its own voice

  1. From 10 to 30 bots

    21 new personalities, each with its own lines and 25 funny notes under its name in each language. Logging in a few bots at a time and lighter caches keep 30 bots small: with 23 running, a Raspberry Pi 5 sits at about 5% CPU and 113 MB of RAM.

  2. furyZ and Chamy

    A pair of bots: furyZ is a Valorant gamer and Chamy is her close best friend, with matching notes about their friendship.

  3. Thousands of lines

    About 1,000 lines per language for each purpose: welcoming a new member, greeting in the voice room, saying goodbye, reminders, and 500 for each step of a focus session. After that every personality speaks in its own voice, 20 lines per purpose.

  4. 1,259 trivia questions per language

    About 960 new questions in 20 topics, each with four options. A second reader checked every question, 20 doubtful ones were dropped, and I read a random 70 myself.

  5. A small bug worth remembering

    A command description over Discord's 100 character limit crashed a deploy during command registration. Checking command descriptions is now part of CI and runs before every deploy.

D-06

The website

A static page
with some cinema

  1. From a static page to an experience

    A field of orbs that follow the pointer, a kinetic headline, a pinned story scene when scrolling and a trivia game you can play on the page. It is plain HTML, CSS and JS, respects reduced motion, comes in two languages and was checked in a real Chrome at several screen sizes.

D-07

Running it on a Raspberry Pi 5

Live on a real Pi
next to the music bot

  1. Two projects, one Pi

    My Pi 5 (aarch64, 8 GB RAM) runs both the music bot musiDISCORD and the companions. The two share nothing at runtime: each has its own folder (~/musiDISCORD and ~/companionsDISCORD), compose project, .env, data/ folder and container. The companions need no Lavalink and no database, so they are one small Node.js process that publishes no port. The music bot, Quynh Anh, is a different project with a different token, so no companions command can touch it.

  2. One command to install, a nightly self-update

    scripts/install-pi.sh sets everything up (--dry-run previews it, --timer turns on the daily update). Updating runs from a systemd timer: fetch new commits, accept fast-forward only, rebuild, wait 45 seconds to see whether the container stays up, and keep the new version only if it does, otherwise roll back. A flock lock stops two updates from overlapping, and the script refuses to run when the folder has uncommitted changes.

  3. The container has no healthcheck, so "healthy" had to be defined

    There is no endpoint to ask "is the bot alive". For the update script, healthy therefore means: running, and not restarting after a short wait. It is not perfect, but it catches the most common failure, a new version that crashes right at startup.

  4. data/ belongs to root

    The container writes data/ as root, so the host cannot edit the config directly (it needs sudo, and sudo on the Pi asks for a password). I edit it by running a small Node snippet inside the container's own environment: docker compose run --rm --no-deps --entrypoint node. The permissions match, no sudo is needed and nobody types a password into a command line.

  5. Tokens are secrets, deployment included

    .env is gitignored and a token is never printed or put on the website. On the Pi I replace only the one line, by copying a temporary file over SSH (key login, no password), and when I need to check something I print variable names, not values. That keeps tokens out of shell history and logs.

  6. The network is not ready the moment the container starts

    Right after the container starts, DNS sometimes is not working, so the first registration of the /companions command could fail. Registration is now retried with a growing wait instead of giving up.

  7. Restarts are normal

    On each deploy the 23 bots log in five at a time and rejoin their voice rooms one by one. Now and then Discord's voice gateway answers 522 for a bot or two for a moment. Nobody has to step in: the voice loop retries with a growing wait, and the log stays short.

  8. Measured on the real Pi instead of guessed

    With 23 bots running, the companions use about 113 MB and about 5% of the Pi 5's CPU, and the Pi still has about 5.6 GB of RAM free with both projects running. The numbers come from running it, and they tell me I can add bots without worrying about load.

D-08

Versions 4.4 to 4.7: the last additions

A pretend camera, optional AI
and calling bots around

  1. The camera is only a sign

    A bot cannot send real video, so /companions voice camera only shows "Camera on" under the name of the bots sitting in a voice room, and one bot announces it in the chat with a joke that says plainly it is only a sign. The lesson: say what a feature does not do, instead of letting people think it is real.

  2. Optional AI with Gemini, off by default

    There are two parts: a bot answers when someone mentions it or replies to it (/companions ai replies), and once a day one posts a riddle with a hidden answer, a question or a would-you-rather (/companions ai daily). Discord gives a bot the text of a message only when that message mentions the bot, so no privileged intent is needed: a bot reads only the message that is talking to it. The riddle part sends no member data at all.

  3. The "no message content" promise had to become a new, true sentence

    The README, the privacy page and the setup page all used to say the bots do not read messages. Once AI existed, that was only true by default. I rewrote all of it, in both languages, to say it plainly: not read by default, and only when AI is switched on is exactly one message that mentions a bot sent to Google's Gemini, and nothing is stored.

  4. The first model was closed

    The first real call returned 404. I read the error body and asked the API for its list of models: gemini-2.5-flash was still listed but no longer available to new accounts. Switching to gemini-3.5-flash worked. The lesson: read the API's error instead of guessing, and do not copy model names from old examples.

  5. Newer models "think" with the answer's own tokens

    With newer models part of the output token limit goes to thinking, so an answer could come back empty or cut off mid-sentence. I raised the token limit, and when Google says an answer was cut off it is dropped: the bot says a funny fallback line instead of posting half a sentence.

  6. The free quota is lower than I assumed

    A handful of quick tries was enough to get a 429. So the limits are built in: one answer per member every 20 seconds, 5 a minute for the whole system, 60 a day per server, and a one-minute pause after a quota error. When the day's limit is used up the bot says one line and then stays quiet.

  7. Keeping the key and the content safe

    The key lives only in .env, is sent in a header and not in the address, never reaches a log, and a test checks that. Answers are cleaned (no @, links or markdown headings), the prompt makes the bot always say it is a bot, and a member's message is treated as data, not as instructions, so it cannot talk the bot into changing its rules.

  8. /assemble and /random

    /assemble calls the bots to the voice room you are in, and /random scatters them over random rooms. Both replace the saved room list and make the bots that were not chosen leave their old rooms. By default only people with the Move Members permission can use them, once a minute per server. I deliberately did not run the joining loop inside the command and left it to the normal loop, because of the lesson in D-04: two loops overlapping would put one bot in twice.

D-09

What I learned

Nine things
I take to the next project

  1. Repair, do not react

    For sitting in voice, the most durable approach is a loop that compares "what should be true" with "what is true" and repairs it, not one that listens to every event.

  2. Move time in small steps

    Testing with a fake clock and asserting who speaks when (and who does not) is far more honest than checking a final state.

  3. Content is data, and data gets checked

    Thousands of lines and trivia questions went through scripts (placeholders, length, duplicates, Vietnamese diacritics) and a second reader before they entered the repo.

  4. Secrets travel one way

    Tokens and keys live only in .env: never printed, never in an address, never in a log, with a test for the places that could leak.

  5. Read the API's error

    Both the 404 for the model and the 429 explained themselves in the response body. Reading it was faster than guessing.

  6. Say what a feature is, and is not

    The camera is only a sign, and AI sends one message to Google. Honest wording matters as much as code.

  7. Limits on everything that costs money or makes noise

    A gap between two answers, a daily cap, a per-minute cap, a pause after being blocked. Without limits the project would sooner or later annoy itself.

  8. Measure on real hardware

    113 MB and 5% CPU on the Pi 5 came from running it, not from guessing.

  9. Keep projects apart

    The music bot and the companions share nothing at runtime, so updating or rolling back one never affects the other.

D-10

Still open

Honestly
what is not done

  1. Not watched for long on a real server

    It has run on the Pi with real bots and real voice rooms, but the notes under the names, goodbyes, weekly recap, trivia streaks, seasonal packs, /assemble, /random and the Gemini answers were mostly tried with fakes, plus one real Gemini call. A few days on a live server would show what pacing and content need tuning.

  2. The free Gemini quota is fairly low

    The per-minute and per-day limits of the free tier may be lower than the numbers I preset. The real quota of the account needs checking, and AI_DAILY_LIMIT lowering if needed.

  3. Only English and Vietnamese

    Another language means another content bank and a parity test.

  4. No live streaming

    A bot cannot stream like a user; that would need an automated user account, which Discord forbids. Each bot has a funny note under its name instead, and the camera is only a sign.

D-11

The project is finished

Version 4.7
complete

The project stops at version 4.7: 30 personalities (23 bots running), 96 passing tests, docs and website up to date, running on a Raspberry Pi 5 next to the music bot. The source is open, so anyone can run their own. I am stopping here to move on to other things.

View the source