Platform

Interaction and plugins

Members talk to the bot the way they talk to anyone else in the group: by name, in English or German, with their typos forgiven. Everything behind that is deliberate about when it answers, when it stays quiet, and what it is permitted to understand at all.

How a member addresses the bot

The wake word is the bot's own name, and the name belongs to the deployment: CIND3R3LLA is the product, and each community names the bot it runs. Because the trigger is a name rather than a fixed phrase, it works in any language without a rule per language. A greeting in front of it is optional decoration and gets stripped, so "Hallo Cinderella" and "Good morning Cinderella" land exactly like the bare name. The greeting list is a setting, shipped with the common English and German ones, and an operator adds their own.

The name has to be the first standalone word. Only a short prefix may precede it, a greeting or a discourse filler such as "so" or "ok", at most three words and twenty characters. "I think Cinderella is great" is therefore not an address, and neither is a possessive or a German compound such as "Cinderellas Archiv". That last case is exactly why fuzzy matching cannot be naive: the suffixed form sits one edit away from the name, so plain edit distance would forgive the one case that must be ignored. The matcher forgives "cinderela" and "cinderlla" and refuses anything that is the name plus something.

Typo tolerance is tiered by word length: three characters or fewer must match exactly, up to six absorbs one slip, longer words two. In a short word a swapped letter is not a typo, it is a different word.

Once a conversation is open, a member has sixty seconds to follow up without repeating the name, and a direct reply to one of the bot's own messages counts as an address on its own. Two modes are available: relaxed, where a leading name is enough, and strict, where a greeting is required before it. Direct replies, the follow-up window and the slash commands are unaffected by strict mode.

Shortened forms of the name are recognised in the same position and answered with a retort, twelve per language, shipped and editable. They are never resolved and never act on anything, and after three in a row the bot goes quiet.

In doubt, silence. A missed address is a small annoyance; an unwanted interjection in a busy group is not.

English and German, both first class

Normalisation is deliberately aggressive, so that a member's typing habits do not decide whether they are heard. Case is folded, German umlauts are expanded the way people type them without a German keyboard (ä becomes ae, ö becomes oe, ü becomes ue, ß becomes ss), remaining diacritics are stripped, and punctuation is not part of a token. "veröffentliche" and "veroeffentliche" become the same string, which is what lets one keyword list cover both spellings.

Every intent carries phrase and keyword sets in both languages, and a longer contiguous phrase always outranks a single keyword. "stop publishing" is read as a withdrawal, not as "publish" with a stray word in front of it.

Which language to answer in is decided by a scored contest between two hint sets, with a margin the winner has to clear. The first version simply asked whether any German looking word was present, and it fired in the live group on a 357 word English announcement that contained "hallo" once, inside its own example. One token in 357 chose the reply language. Where a keyword set matches unambiguously, that match is the stronger evidence and the answer follows it; the detected language is remembered for the length of the follow-up window; and an operator who wants one language only can fix it.

Persona copy is data rather than code. Her persona strings and her retorts exist per language, are editable in the admin console, and fall back to the shipped wording when a field is left blank. The consent notices and the publishing properties inside the help reply stay in code on purpose, so what a member is told about publishing cannot drift from what actually happens. Adding a language is adding a key for her persona replies. The help reply still ships in English and German only, so a newly added language answers help in English until that copy is written.

Both languages ship as defaults with their own keyword sets and their own copy. Neither is a translation layer over the other.

Three slash commands, and one route no setting can close

Slash commands are shorthand, not a second product. There are exactly three. /publish and /unpublish are matched exactly, as the whole message; /help is matched as its own word, so /helpdesk is not caught. Anything else beginning with a slash is not treated as an instruction at all.

The command route is immediate: it does not ask the confirmation question the spoken route asks, because typing the command is already unambiguous. Both routes share a single consent write path, so an opt in by command and an opt in by conversation are journalled, audited and undone in exactly the same way.

There used to be a setting that switched slash commands off. It is gone. Its entire reach was those two consent commands, and with it off /unpublish neither acted nor replied: a member who types it and sees nothing reasonably concludes that it worked, while their words stay public. The setting was removed rather than left inert, because a control whose label promises more than it can do is worse than no control at all.

Natural addressing itself can still be switched off, for an operator who wants a quiet bot in a busy group. The consent commands keep working, so the withdrawal route survives every configuration.

Withdrawing consent is not a convenience feature. No setting can take that route away.

A closed catalog, and a resolver that executes nothing

There are nine intents and no tenth: publish, unpublish, restore, status, search, price, help, undo, and not understood. The set is closed in the type system, and every result is validated again at the seam that all callers go through, against the catalog as it stands at that moment. A resolver that returns something outside it has said "not understood", never "go ahead".

A resolver reports what it believes was meant. It never acts. The engine performs the action and the consent code decides whether it is permitted, and that separation is precisely what has to survive when a model takes over the understanding.

Three consequences a member notices. A spoken consent change always confirms first, and the offer lives exactly as long as the conversation does. The typed command stays immediate, because typing it is already unambiguous. A request on somebody else's behalf is refused outright, including from an administrator, because consent is first person only. And a question about state stays a question: "what is my publish status" is answered with the status, rather than putting a consent prompt in front of somebody who only asked.

When the bot says nothing

Doubt is expressed as silence or as a question, never as a guess. A hypothetical framing ("what happens if I say ..."), a negation within three tokens of the match, or a keyword inside quotation marks collapses the score, and anything below the configured confidence, 0.55 by default, counts as not understood. Above two hundred characters an instruction is acted on only at high confidence, because a command is short and a pasted article that happens to open with the name is not one.

Every guard that produces silence also makes the bot look broken from the outside, so each ignored candidate is recorded with its reason, forwarded message, weak signal, too long, strict mode without a greeting, and shown in the admin console. These are diagnostics, not a record: held in memory, capped at fifty, truncated, and gone after a restart.

How answers appear is a setting as well. Plain by default; mention prefixes the member's name; quote repeats the message being answered. Consent prompts never quote, whatever the mode says. Replies are rate limited per member and per chat, six and twenty a minute by default, and a display name has its formatting characters stripped before it is used, so a name cannot open a span that swallows the rest of the sentence.

The plugin boundary

A plugin declares four things and nothing else: who it is, whether it is on by default, which intents it contributes, and where its settings page lives. Adding one is that declaration plus a settings page, with no change to the sidebar, the resolver or the settings framework. Plugins are part of the build; there is no runtime drop-in directory.

The load bearing property is what happens when a plugin is switched off. Its intents do not remain in the catalog to be refused later, they leave the catalog. The rule engine never even considers the disabled plugin's patterns, the seam downgrades anything claiming its intent to "not understood", and the help reply, which is generated from the live catalog rather than written out as a list, stops advertising the capability. A half wired handler behind a disabled switch is exactly the thing that answers a question it should not.

A disabled plugin does not go quiet. It leaves the intent catalog.

Crypto Prices, the shipped example

The first plugin answers price and conversion questions from a chain of market data providers. Prices are always fetched on request, never preloaded. The only thing between a question and the provider is a short cache, and it exists to keep a busy group from burning the provider's rate limit, not to make a price last longer.

Symbols are resolved once and then pinned. Asking a provider "what is HEX" every time would mean the same question quietly returning a different token's price once search rankings moved. When more than one asset genuinely claims a ticker, the member is asked and answers with a number, and that answer is pinned so nobody is asked again. Where the leading candidate outweighs the next by a hundred to one, it is pinned without asking, because whether someone meant Bitcoin or "Bitcoin AI" is not a real question; an operator who disagrees sets that factor to zero and is always asked.

The resolver never learns which symbols exist; that is the registry's business. It hands over the words the member actually wrote, plus the other candidates in the sentence, and the price service prefers whichever is already pinned. That is what turns "one real bitcoin" into Bitcoin rather than an unknown token called "real", and it is why an operator can add a token without a code change.

Inside the follow-up window a fragment can inherit the previous question, so "monero?" after a price answer is a price question. It may reuse knowledge and never create it: both the asset and the currency must already be pinned. The rule exists because applause once inherited a price question, went to a provider as a ticker, and came back offering the member a choice between two real tokens.

Where a local model is allowed to help

The deterministic rule engine is the standing implementation and the automatic fallback. When an operator connects a local Ollama runtime, a model can be registered behind the same seam; if it fails, is slow, or answers outside the catalog, the rules answer instead. With the runtime off, the rules are simply what runs.

The model classifies. It never executes, writes consent, calls a tool, or decides whether a confirmation was accepted, and a consent intent is accepted from it only when the rule engine independently found the same intent. A model mistake can therefore cost an understanding. It cannot cost a publication.

A second model may phrase a finished reply. It has no database, consent, tool or transport capability and receives a draft the engine has already produced. Values that must survive a rewrite, counts and prices, are checked for; values that must not appear, such as the sender's display name, are checked against; and dashes in the output are rewritten, because no member facing string in this product carries one. If a check fails, the deterministic wording is sent.

A native command menu, generated but not connected In development

The SimpleX SDK can register a bot command menu that a client renders in its compose bar. It is a one to one affordance: the bot has no contact address and members only ever meet it in the group, so registering the menu would publish a list nobody could reach.

The producer exists anyway and is fed from the same active catalog as the text help, so the day the bot is given a direct chat surface, the menu is one line away and already reflects exactly which plugins are enabled. Until then, the help reply is the command list.