Platform

NPCs and characters

A court jester that makes a joke about a harmless moment, a quiz host, a welcome character. The personality is the visible part. When a character may speak, to whom, how often and with what access is decided by application code, and the model only writes the sentence.

Characters, not traffic generators In development

The roles a character can take are ordinary community jobs: a court jester, a quiz host, a storyteller, a tutorial guide, a welcome character for new members, an event host, a community mascot, and a commentator that appears on a schedule. Each one is a persistent identity with its own name, its own voice and its own permissions, not a general purpose bot posting into every room.

The court jester is the example worth holding onto, because it shows exactly where the line runs.

Everything in that sentence except the joke itself belongs to application logic, not to the model. Two of those boundaries run today: whether it may speak in this group at all, and how often it may post before it has to stay quiet. When it appears, and what it is allowed to read, are the parts still being built. The model receives a finished decision and writes one sentence of it. The model receives a finished decision and writes one sentence of it.

One character runs today, Cinderella herself, in her archivist role. The roster above, and the personality and scheduling engine that gives each character its own rhythm, are being built on the boundaries described below, which already run in production.

The court jester appears at irregular times, makes a context aware joke about a harmless moment in the conversation, and can be addressed directly for another joke.

Addressed, or silent

A character speaks when it is spoken to. The wake word is the name itself, which is what makes addressing work in any language without a rule per language: a greeting or a short filler in front of the name is stripped, so "Cinderella, publish me", "Hey Cinderella publish me" and "Guten Morgen Cinderella" all reach the same instruction. A real greeting does one thing more: it is the strongest signal that she was actually being spoken to, and the operator can require one.

The anchoring is strict on purpose. The name has to be the first standalone word. "I think Cinderella is great" is talking about her, not to her. A possessive or a compound such as "Cinderellas Archiv" is not an address either, and that is precisely why fuzzy matching cannot be naive: a plain edit distance would forgive the one case that has to be ignored. Typos like "cinderela" are forgiven, the name plus a suffix is not.

When a message is addressed to her but the request is not understood, she never guesses. If the address signal was weak, such as a bare leading name, she stays silent and records the near miss. If she was plainly being spoken to, she says she did not catch it and names what she can do. After a reply, a short follow up window lets that member keep talking without repeating the name, and the window also remembers the language the exchange is being held in, so a bare "yes" is answered in the right one.

A missed address is a minor annoyance. An unwanted interjection in a busy group is not.

Message limits and cooldowns

Two rolling limits apply to ordinary replies, measured over one minute: six replies to the same member and twenty in the same chat, both configurable by the operator. Replies that carry a consent decision are the deliberate exception: a confirmation or a revocation answer is always delivered, and still counts against the budget. Expensive work, such as a market data lookup, is metered on its own separate budget on top of that, tighter per member and tracked independently of ordinary replies.

Cooldowns also cover being poked. If somebody addresses a character by a nickname it has not accepted, it answers with a retort, never the same one twice in a row in the same chat, and after three in a row it goes quiet. The streak is forgiven after ten minutes.

None of this state is stored. It lives in the process and is deliberately forgetful, because a durable record of who spoke to the bot and when would be a side channel about members. A restart costs one repeated wake word, which is the right trade.

An apology for being rate limited is still a message in the group, so above the limit the character stays silent instead. The one exception is a reply that carries a consent decision, which is never dropped.

Permissions before personality

Before any character logic runs, an incoming group message resolves to a profile, a group and a member role. The roles are owner, administrator, moderator, team member, member, auditor and blocked. The result is allow, deny or unassigned, with a recorded reason: profile disabled, group disabled, member blocked, or group not assigned.

Every decision is written to a journal keyed by the group and the message, so what was allowed, for whom, and under which profile can be reconstructed afterwards. Two capabilities are fixed to no inside the decision itself and cannot be switched on by configuration: remote commands, and persistent changes made from chat.

A group that has not been assigned to a profile keeps the previous single identity behaviour, and its decision is recorded as unassigned with enforcement marked as not applied. The gap stays visible in the journal instead of hiding behind a default.

Permissions are resolved before personality, so a character cannot talk its way into a capability it was never granted.

The model writes the wording, nothing else

Local models phrase, they do not decide. By the time a model is involved, the intent has been resolved, the database reads have happened, and a complete deterministic reply already exists.

There are two modes. In free mode the model rewrites that finished draft. In locked mode it writes only a short lead of at most 180 characters and the application appends the draft unchanged. Values that must survive a rewrite exactly, such as counts and prices, are checked in the output. Values that must never appear, such as a sender's display name, are checked too. Code fences and control characters are stripped, because this is untrusted output on its way into a chat.

The reply module has no database, consent, tool or transport access at all. If the model is unreachable, or the output fails a check, the deterministic draft is sent as it stands. Those fallbacks are counted and the count is shown in the admin console, so a model that has quietly stopped working looks different from a model that was never enabled.

The model receives a finished answer and writes one sentence of it. It cannot grant itself authority.

Rhythm and scheduling In development

A character that appears on a rhythm needs a scheduler that survives a restart and never fires twice. That part exists: a durable job queue in PostgreSQL, with a scheduled run time per job, lanes and priorities, exponential backoff, a dead letter state, an idempotency key, and a claim that uses row level locking so two workers cannot take the same job.

Today the queue runs archive work. The engine that turns a configured rhythm, quiet hours and an appearance budget into actual character appearances is the piece currently being built on top of it.

Moderation aware silence In development

The design is straightforward: a character stays out of a moment that is being moderated. A joke landing in the middle of a report is the failure this prevents.

In the code today, moderation state sits on the publication path. It decides what reaches the public archive and it is audited there. The interaction layer does not read it yet, so a character does not currently fall silent because of moderation activity. The hook it will read is already in place.

Names, avatars and generated personality In development

Identity generation is deterministic first, with the model as an optional creative layer. The name generator is built and verified: a seed plus a configuration produces a name, the same seed always produces the same name, culture grammars assemble the parts, particles such as "van der" keep their lower case, casing can be natural, lower or mixed, frequencies follow a population distribution rather than a uniform draw, and the result is sanitised to what SimpleX accepts as a display name.

One honest limit belongs with that: the shipped corpus carries no culture labels, so the grammar engine currently runs against small hand authored pools. Culturally coherent names arrive with a labelled corpus, and the swap point is marked in the code.

Avatars today come from the operator. An image is downscaled to a square JPEG small enough for the SimpleX profile envelope, and the core applies it on the next boot. The deterministic avatar generator, and biography and avatar generation with local AI, are specified rather than built. Generated results will be cached with their seed and generator version so a profile stays reproducible.

Where this leaves you

If you are deciding whether to run this: what you can put into a group today is one character inside the boundaries above, on hardware you control, with the limits, the roles and the persona text editable in the admin console and stored in your own database.

If you arrived from a chat link: a character talking in your group does not decide what gets published about you. That is a separate consent path, first person and opt in, and nothing a character says changes it.

If you are evaluating the project technically: verify:interaction and verify:runtime-policy run against a real PostgreSQL compiled to WebAssembly, with no server needed, covering addressing, limits, silence and the permission decisions. verify:ai-replies exercises the wording guards against a stub model and an in-memory store, and verify:namegen covers determinism and the population statistics with no database at all.