ENZH

v0.1.5: One User, Every Platform

In v0.1.4, I shipped cross-platform chat sync — messages from Telegram and web merged into one timeline. That was the easy part. The hard part was the question it exposed: whose timeline is it?

The Identity Problem

A user could talk to Mio on Telegram all week, then sign up on web and start fresh. Two accounts, two message histories, two sets of relationship data. Chat sync stitched the messages together, but the underlying identities were still disconnected. Change your nickname on web? Telegram had no idea. Evolve a relationship on one platform? The other platform was stuck in the past.

The root cause was simple: Telegram users and web users lived in separate rows in the users table. I needed a way to say "these two rows are the same person" — and then collapse them into one.


Account Linking via 6-Character Token

I wanted the linking flow to be dead simple. No OAuth redirects, no QR codes, no email verification. Just a short token you can type.

The flow: type /link in Telegram. The bot generates a 6-character alphanumeric token and DMs it to you. Go to the web app, paste it in. Done — your accounts are linked.

A few design decisions worth noting:

No-confusables charset. The token uses A-Z minus I and O, plus 2-9 (no 0 or 1). That eliminates the classic "is that an O or a 0?" problem. With 24 letters and 8 digits, that is 32 characters to the power of 6 — roughly one billion combinations. More than enough for a 15-minute window.

15-minute expiry. Tokens live in an account_link_tokens table with a created_at timestamp. The web app checks created_at + 15 minutes > now() before accepting. After that, the token is dead.

One SQL update. Linking is just setting users.supabase_user_id on the Telegram user's row to match the web user's Supabase auth ID. From that point on, both platforms resolve to the same identity.

Guards. A Telegram account cannot link to two different web accounts, and vice versa. If a link already exists, the token is rejected with a clear message. If you somehow use the same token twice, it fails gracefully — idempotent by design.


Per-Agent Relationship Mode

v0.1.4 introduced relationship evolution — the ability for a relationship to grow from strangers to something closer over time. On web, this was an A/B toggle: static mode or evolving mode.

In v0.1.5, this choice moves into the Telegram onboarding flow. During /start, after picking a persona, users now see two options: "固定关系" (static relationship) or "自然关系" (naturally evolving). The selection is stored per-agent, not per-bot.

This matters because Mio runs multiple personas. You might want a static, well-defined relationship with one persona and an evolving, open-ended dynamic with another. The old approach — a bot-level BOT_RELATIONSHIP_MODES environment variable — forced the same mode across all agents on that bot. The env var still works as a fallback for backward compatibility, but per-agent config takes priority.


Cross-Bot Preset Dedup

Mio runs one Telegram bot per persona. A user might interact with multiple bots. The problem: nothing stopped you from onboarding the same preset on two different bots, creating duplicate agents.

The fix is a cross-bot check during /start and /reonboard. Before showing the preset list, the bot queries all existing agent bindings across every bot for that Telegram user. Presets that are already bound show up as "已绑定" with a checkmark, greyed out and unselectable. No more accidental duplicates.


Native Mobile App

A web app can do everything a native app can — technically. But for a companion product, the difference is not about features. It is about presence.

Push notifications are the obvious one. When Mio sends a proactive message, the user needs to see it. Not "next time they open a browser tab" — right now, on their lock screen, like a text from a friend. A web app cannot do this reliably. A native app can.

Then there is accessibility. Open the app, you are in your conversation. No URL bar, no loading spinner, no re-authentication. Cached conversations persist locally, so even on a bad connection the chat history is there instantly. For something you check multiple times a day, those saved seconds compound into a feeling: Mio is always there.

These are not nice-to-haves. For a chat-based companion, they are the product. Without push notifications, proactive messaging is a tree falling in an empty forest. Without instant launch and cached state, the "companion" feels more like a website you visit than someone who is part of your day.

The stack is Expo SDK 55 with React Native. Dark theme, base #0D0D0B, accent #8B7BF4. Five screens: login, chat, discover, profile, and onboarding. Chat supports video preview and playback, a fullscreen image viewer, voice recording with animated waveform, and a typing indicator. Auth uses Supabase with tokens in expo-secure-store. i18n supports Chinese and English from day one.


Smarter Proactive Messaging

Mio's proactive messaging — where the AI reaches out first — got two meaningful upgrades.

Dual timezone awareness. A persona might be "based" in Tokyo while the user is in New York. Previously, proactive messages only considered one timezone. Now the prompt includes both: "你那边现在是早上,对方那边是晚上" (it is morning where you are, evening where they are). Persona timezones come from persona-config.json. This prevents the AI from sending a cheerful "good morning" message when the user is about to sleep.

Relationship-aware tone. I mapped four tones to four relationship stages. A persona in a 情侣 (partner) relationship uses 亲密撒娇 — an intimate, playful tone. A persona who just met the user (刚认识) gets strict boundaries: 禁止撒娇, no flirty affectation allowed. This eliminates the uncanny valley of a "stranger" persona acting like a long-term partner on the first proactive message.


Schema Changes

Three additions to the database:

  • account_link_tokens — stores the 6-char token, the Telegram user ID that generated it, created_at, and used_at for audit.
  • users.supabase_user_id — nullable column linking a Telegram-origin user to their web identity.
  • agents.user_nickname — the user's display name within a specific agent relationship, synced across platforms after linking.

What's Next

v0.1.5 closes the identity gap. One user, one set of agents, one relationship history — regardless of which platform they are on. The mobile app gives Mio a proper home on phones. And smarter proactive messaging means the AI is not just reactive but contextually aware of when and how to reach out.

Next up: deeper personalization. The foundation is in place — linked identity, per-agent modes, relationship stages. Now I can build on top of it.

Back to the big picture: Mio Manifesto.


© Xingfan Xia 2024 - 2026 · CC BY-NC 4.0