Skip to content

Introduction ​

The ancients believed that anything, given enough years, can become a spirit — hence jing (精); and because such spirits can bewitch the human heart — mei (魅).

Jingmei is an AI group pet that lives in Discord and Telegram groups. One configuration can keep several characters with different personalities: they chime in by probability, always answer when addressed, understand images and videos, send voice messages, look things up on the web, do math, and remember members' birthdays. Both platforms share one conversation core, and every character uses its Pi session in every channel as a "conversation segment": only a triggered character writes to its own session, a running conversation continues the same segment, a long gap or a large backlog starts a new one, and anything older is found by search.

What it does ​

  • Content-aware participation, always routes when addressed: explicit mentions, replies and names/aliases retain their routing priority. With a decision client and participation enabled, other human messages identify a directed character; otherwise a deterministic routingP candidate must pass cooldown, speaking-share and, when enabled, content-score gates. Bot messages never trigger a character. Final text is checked before sending to withhold internal markers or unnatural replies.
  • Failures do not stall the channel: model/tool turns have a 180-second deadline, then abort and release the channel. Failures are logged without sending an error notice to the group. Successful Telegram text/image/voice replies are also stored in history so replies to the bot can inherit the original topic; Discord remains echo-driven.
  • Crash recovery without stale chime-ins: history and pending work are stored in one transaction. On restart, unfinished messages up to 3 minutes old are processed. Failed turns are not retried. A message already over 3 minutes old when it arrives or reaches its channel queue (including a backlog delivered after a long outage) is only stored and indexed for search: it never gets a reply (even when mentioned, replied to or named), a participation decision, a quick reaction, member-memory or topic processing, and Telegram skips downloading its photos and videos, keeping [图片] / [视频] markers. Such messages still enter the conversation as accumulated history when a character is next triggered. Recovery preserves text and replaces images with an honest unavailable-image note.
  • "Typing…" stays visible: while a routed character generates its reply, the typing indicator is refreshed every 4 seconds on Telegram and every 8 seconds on Discord; it stops when the reply is sent, withheld or fails, and never shows for more than 60 seconds.
  • "Why didn't it reply?" is answerable: every human message gets one route log line without its text, giving the final route, the HMAC candidate, whether cooldown/speaking-share blocked it, and the participation decision and score; paused messages log paused. Fields: docs/architecture.md (Chinese).
  • Jev quick reactions (optional): a Jev decision API (or an in-process LLM wrapper) puts an emoji on messages. Addressed messages get the selected emoji unless the decision is none; ordinary messages only when strongly emotional or genuinely funny, rate-limited per channel. It does not use the main model and never delays the real reply. See Jev.
  • Concurrent topics (optional): events assigns channel messages to topics. §E IDs, titles, descriptions and leading participants tell the character which event it is answering, keeping simultaneous discussions separate and recalling older topics when they resume.
  • Conversation segments, continued on demand: every group message is stored and indexed, but only the character the router triggers writes to its own session; messages that trigger nobody enter no session. When a character is triggered, its previous session continues if its last successful reply was at most 5 minutes ago, at most 30 messages have accumulated since, and the session is at most 40,000 tokens: the accumulated messages are appended as one context message, then the trigger. Otherwise a new segment starts, seeded with the 30 most recent messages before the trigger. Each line looks like [time] #messageId ↪ replyId §E<topic> author: text (相关 N 条), where N is the number of related messages in earlier history (shown as 20+ above 20). It is fixed when the line is written and never recomputed, so the prefix stays stable and cache hits survive. After a turn timeout, send failure or final error, the next trigger always starts a new segment, also across restarts.
  • History search: when a line shows "(相关 N 条)", a character can call related_messages to see the earlier related chat for a message, or search_history to search by keyword and meaning (optionally limited by ISO time or YYYY-MM-DD date). The two share at most 3 lookups per turn, each returns at most 20 lines, hits come with 2 lines of context on each side, only the current group/channel is searched, and members who used /forget never appear. Keywords use SQLite FTS5 trigram; meaning uses sqlite-vec vectors (controlled by features.history, on by default, using the default embedding model; events shares it). Messages stored before the upgrade become searchable only after running the backfill script.
  • Images and video frames: up to 4 images per message, scaled to fit 1024×1024 and 200 KB, reach the model; videos are sampled into 1–3 frames with ffmpeg. When the main model has no image input, Pi replaces each image with an omission note; alternatively set visionModel to describe images in a sentence or two first. Voice, files and stickers become text placeholders such as [语音], [文件], [贴纸 😀].
  • Voice (optional): with Fish Audio, characters can send MP3s with a transcript in Chinese, Japanese or English. When a member explicitly asks for a voice reply, the final answer is also turned into audio.
  • Drawing (optional): once the Antigravity provider is signed in, characters can put an image part in send_reply to draw one picture from a member's description and send it (Nano Banana 2 / gemini-3.1-flash-image by default, about 15 seconds). See Drawing for sign-in. Warning: Google's Antigravity terms explicitly forbid third-party tools using Antigravity OAuth and accounts have been banned for it; use a secondary account.
  • Long text as an image (optional): with textImage enabled, a text reply over 300 characters (configurable) is not sent; the character is told to use a text_image part of send_reply instead, which renders its full Markdown to one image: headings, lists, tables, code blocks, LaTeX math ($…$, $$…$$), public images, graphs (functions, implicit curves, parametric curves, scatter points, 3D surfaces) and, when drawing is enabled, pictures newly drawn by AI. Pure libraries, no browser. See Long text as an image.
  • Candlestick charts (optional): with kline enabled, a character can put a kline_image part in send_reply to send a live candlestick chart with volume for a Binance spot pair (for example BTCUSDT). The bot fetches the prices from Binance's public API; the model only picks the pair and interval and never writes numbers. Pure libraries (hand-written SVG + resvg), no browser. See Candlestick charts.
  • Web search: with DEEPSEEK_API_KEY set, DeepSeek server-side web search is enabled. Messages that explicitly say “查一下” / “搜索” / “look up” are searched first and answered with source links; the model can also search on its own when a question depends on external facts.
  • Calculation: run_js runs small pure-computation JavaScript in a node:vm realm inside a short-lived child process for exact arithmetic, date math and unit conversions. It requires Linux with a working bubblewrap (filesystem, network and PID isolation); startup runs it once and exits with an error when it is unusable, with no fallback. No new configuration is needed. See docs/deploy.md for Ubuntu setup, and docs/architecture.md for residual risks.
  • Member memory and soul: per group, profiles hold names, birthdays, stable self-stated facts, and relationships formed by mentions and replies. Member memory is not attached to the input automatically; the replying character calls recall_member_memory with a chat display name when it needs it (bots and members who used /forget are excluded), keeping history shorter and the cache stable. Characters save explicit self-stated interests, roles, projects, timezones, languages, goals, and preferences, never reciting full profiles or birthdays publicly. Each character keeps a private soul per channel; stable lessons about its own formatting, tone, or response length are staged and become formal when a new conversation segment starts or after successful compaction. Members can /forget at any time. The system prompt spells out when to save and when to recall: save before replying when the author states lasting facts about themselves (never jokes, passing states, other people's details or sensitive data); recall before answering about a member; if nothing is found, say so instead of inventing memories.
  • Holiday and birthday greetings (optional): sent to a chosen channel after 09:00 local time, covering birthdays and Chinese/Australian holidays; deliveries are recorded in the database, so restarts never resend.
  • Reaction images: characters can send one of 4 bundled PNGs (hello, laugh, think, hug) or an image from their own local PNG/JPEG catalog (a reaction_image part of send_reply), using its default caption; can be turned off per character.
  • Several messages in one reply: with send_reply a character sends up to 4 messages in order in one turn: text, voice, a newly drawn picture, a reaction image, a text image (at most one of each non-text kind), for example a picture followed by a text or voice explanation, or an answer in a few separate paragraphs. The tool only offers the kinds that character can send. Slow parts (drawing, rendering, speech) are prepared in parallel and sent strictly in order once all are ready; only the first message replies to the trigger. If any part cannot be prepared nothing is sent and the character falls back to text; if a send fails midway, what was sent stays, nothing is resent and the rest is dropped. Text and voice transcripts pass the same internal-marker check, naturalness audit and length gate as an ordinary text reply. A one-message answer is still written as plain text.
  • No thinking by default, fewer tokens: reasoningEffort defaults to off; even when enabled, thinking from completed turns is not sent back to the model. The system prompt and tool definitions stay stable for provider prefix caching.
  • Fixed identity and summary boundaries: characters know their names, aliases, and verified platform accounts and accept turns selected by the router. Media instructions distinguish direct image input, optional auxiliary descriptions, and placeholders without inventing processing. History compaction instructions preserve fixed identity, retain confirmed facts, attribute member claims, and exclude assistant guesses or past refusals as permanent rules; style lessons require an explicit member request.

Architecture at a glance ​

One supervisor and one bot worker; the worker contains one conversation core and two thin platform adapters. Platform differences (formatting, length limits, emoji sets) stay in the adapters; the core only knows InboundMessage (in) and PlatformTransport (out).

PrincipleIn practice
Pi-native firstSessions, near-limit compaction, model catalog and auth, image degradation all come from Pi
Deterministic before LLMReplay-safe HMAC selects probability candidates; history determines cooldown and speaking share. Content decisions use bounded requests; deduplication is a database primary key
Bounded costStable system prompt and tool definitions hit prefix caches; dynamic content goes into messages only; segments rotate at fixed limits, seeds hold only the latest 30 messages and older history is searched; reasoning off by default
Private by defaultSecrets live only in .env; logs are redacted and never contain message text; members can /forget at any time

Full data flow, schema and the run_js threat model: docs/architecture.md (Chinese).

BSD 2-Clause License