v0.1.2: Texting Like a Real Person Now
The Real Test
v0.1.0 gave each persona a voice, a face, and a personality that doesn't depend on labels. But they all lived inside the same Telegram bot. You'd open one chat and switch between personas with commands. It worked, but the illusion broke every time — you're not talking to Keke, you're talking to a bot that's pretending to be Keke.
v0.1.2 fixes the two things that made Mio feel like software instead of people: shared identity and dumb timing.
One Persona, One Bot
The request was simple: "I created a second Telegram bot. I want each persona to have their own bot."
Not a persona selector inside one bot. Not a menu. A separate Telegram bot for each character — their own profile picture, their own name, their own chat thread. When you open your Telegram, you see Keke and Surou as separate conversations, the same way you see separate conversations with real people.
The implementation uses a single env var:
TELEGRAM_BOT_TOKENS=token1,token2,token3
Comma-separated. The server loops through each token on startup, registers a per-bot webhook:
/telegram/webhook/:botId
Each bot gets its own webhook endpoint. When a message arrives, the server knows which bot it came from by the route parameter. The botAccountId becomes a composite key — combining the bot's identity with the user's identity — so onboarding state, chat buffers, and conversation history are all per-bot, per-user.
The constraint that makes this work: one persona per bot per user. You can't bind Keke and Mimi to the same bot. Each persona gets their own bot, their own identity. In the preset selection flow, already-bound presets show "已绑定" with a checkmark — you see which personas are taken and which are available.
This sounds like a small UX change. It's not. When each persona has their own Telegram bot, they stop being "a mode of the Mio system" and become "someone in my contacts." The chat history is theirs. The notifications are theirs. The profile picture is theirs. The psychological framing shifts entirely.
Texting Like a Real Person Now
Proactive messaging existed before v0.1.2. The system would periodically send messages to keep the conversation alive. The problem: it was frequency-blind. Every persona messaged at the same rate, regardless of who the persona was or what your relationship was.
A clingy partner and a reserved acquaintance texting at the same frequency is uncanny. Real relationships have different rhythms. Your partner texts you every hour. A new friend you just met might reach out once a week. The frequency is the relationship.
v0.1.2 makes proactive messaging a function of two variables: relationship type and persona personality.
Each relationship type has a base cooldown:
| Relationship | Base Cooldown |
|---|---|
| 情侣 (lovers) | 1 hour |
| 好朋友 (close friend) | 4 hours |
| 朋友 (friend) | 8 hours |
| 刚认识 (just met) | 12 hours |
Each persona has an eagernessFactor baked into their preset:
| Persona | Eagerness | Personality |
|---|---|---|
| 小柒 | 0.6 | Clingy, expressive |
| 可可 | 0.8 | Warm, chatty |
| 蜜蜜 | 1.0 | Balanced |
| 苏柔 | 1.6 | Reserved, measured |
The effective cooldown formula:
effectiveCooldown = relationship.minHoursBetween × preset.eagernessFactor
小柒 as your partner: 1h × 0.6 = 36 minutes. They text you every half hour. Clingy, present, always there.
苏柔 as a new acquaintance: 12h × 1.6 = 19.2 hours. They reach out maybe once a day. Measured, unhurried, not presumptuous.
The same system, the same formula, but the behavior matches what you'd expect from that person in that relationship. The math disappears behind the personality.
Per-preset proactive.json configs store the relationship-specific cooldowns and daily rate limits:
{
"eagernessFactor": 0.6,
"dailyLimits": {
"lovers": 12,
"closeFriend": 6,
"friend": 3,
"justMet": 2
}
}
Daily limits prevent even the clingiest persona from going overboard. 小柒 might want to text every 36 minutes, but caps at 12 messages a day. There's eager, and then there's harassment.
Exponential Backoff
This was a direct user request: "Your current frequency is too spammy. There should be some sort of exponential backoff — like a real human who's not going to keep bothering you if you don't reply for a long time."
The observation is precise. Real people adjust. If you text someone and they don't reply, you might text again a few hours later. If they still don't reply, you wait longer. After a few unanswered messages, you stop. Not because you lost interest — because you read the silence.
v0.1.2 implements exactly this:
After 3 unanswered proactive messages, the agent goes SILENT.
Not "sends less frequently." Silent. Zero messages. The persona stops reaching out entirely — until you reply.
When you do reply, the backoff counter resets. They're back to their normal rhythm. No grudge, no "where were you," just a natural resumption.
This is the feature that makes proactive messaging feel human instead of algorithmic. Without backoff, proactive messages feel like push notifications — a system pinging you on a schedule. With backoff, they feel like someone who's thinking about you but also respects your space.
The implementation is simple: a counter per agent tracks consecutive unanswered proactive messages. Each proactive send increments it. Any user reply resets it to zero. When the counter hits 3, proactive messaging suspends. That's it. No gradual decay, no probability curves. A hard cutoff that mirrors how real people actually behave — they don't slowly reduce their texting frequency, they just... stop.
Onboarding Cleanup
A smaller change, but it mattered: v0.1.2 disabled the custom relationship type and custom backstory options during onboarding. These were free-text fields that let users type whatever they wanted.
The problem was the same one v0.1.0 identified with personality customization: user-written content clashes with carefully crafted presets. A user typing "we met at a bar in Shanghai" as a backstory conflicts with Keke's actual story of being a Taipei girl who's never left Taiwan.
Now the onboarding flow uses confirm-only buttons. You pick from the preset relationship types — no "other" option, no text field. The relationship_type is persisted as a discrete column on the agents table, making it a first-class data point for downstream features like proactive messaging frequency.
Less choice, better coherence. The same principle from v0.1.0, applied to one more surface.
What Changes
Before v0.1.2: all personas share one bot. Proactive messages arrive at a fixed frequency regardless of relationship or personality. If you don't reply, they keep texting anyway. Onboarding lets users type custom backstories that break character consistency.
After v0.1.2: each persona has their own Telegram bot, their own identity, their own conversation thread. Proactive messaging frequency emerges from the intersection of relationship depth and personality — a clingy partner every 36 minutes, a reserved new acquaintance once a day. And if you don't reply three times, they go quiet. They wait. Like a real person who knows when to shut up.
The formula is elegant because it's invisible. You never see eagernessFactor × minHoursBetween. You just notice that 小柒 texts a lot and 苏柔 doesn't, and that both of them stop when you're busy. That's the whole point — the math should feel like personality.