pyrtan / sourcev0.13.0 · main

pyrtan / _meta / chat.md

11,518 bytes · last changed 2026-10-27 in No seeds: pyrtan.com is GitHub Pages and a hub list, nothing more

Design — Chat: strangers and bots, Omegle-style

StatusBuilt, v0.11; the typing indicator since v0.12. The lobby, bots made of trees, strangers across a follow, the console's Chat tab and pyrtan chat; tests with two hubs over real sockets and the console driven in a browser. "Stranger is typing…", on by default, off per person or per hub (§4).
Date2026-10-27
DecisionA hub started --chat keeps a lobby where anyone who can reach it is paired at random with another person, on this hub or on a hub it follows, or with a bot. A bot is a holder of trees, and what it says is what /intent would have said: the chat adds no model and no new kind of knowledge. Nothing is kept.
Wheresrc/pyrtan/chat.py (the lobby), src/pyrtan/hub.py (/chat, intent(only=…)), src/pyrtan/federation.py (Meet, Say, Part), src/pyrtan/page.py (the Chat tab), src/pyrtan/cli.py (pyrtan chat, hub --chat), examples/small-talk/.

0. The brief

*"A chat/peer section where you can talk Omegle-style with bots or random users on the network."* Read as: press a button, be paired with someone you do not know, talk, press it again for someone else. "Bots" are the swarm's own: Pyrtan has no other kind. "The network" is the hub you are on and the hubs it follows.

1. The lobby

A *seat* is a random id the client holds; holding it is being that seat. A seat says whom it wants to meet (anyone, people or a bot) and up to five interests, and waits. Pairing is at random among the seats waiting here that want people, the ones sharing the most interests first; the system line says what you share ("You both like chess."). A seat that wants anyone and meets nobody in eight seconds meets a bot. A seat that wants people never does.

Everything is a JSON request: POST /chat/start, /chat/poll, /chat/say, /chat/next, /chat/leave, and GET /chat for the counts. poll is a long poll (up to 25 s) on a condition the lobby notifies on every event, and it wakes on its own when the seat is due to ask another hub or meet a bot, so nothing runs in the background. A seat's own lines come back to it as events too, so the log a client shows is the hub's, in the hub's order. A seat nobody polls for a minute is let go and its partner told, which is how a closed tab ends a conversation.

The lobby follows the hub's access rule: with a token, only callers with the token chat. That is deliberate. A bot runs trees, so a lobby open to callers the hub would refuse at /intent would be a way round the token.

A public lobby (v0.13). A hub that wants strangers from the internet in its lobby, and its token on everything else, starts --chat-public ($PYRTAN_CHAT_PUBLIC). /chat then answers without the token, and a caller without it is a *guest*: a guest meets people, on this hub or on a hub it follows, and never a bot. want: bot from a guest is refused (bots-need-token), anyone is read as people, and /chat counts no bots for them. /api says chat_public and, for the caller asking, chat_bots; the console offers a guest "people only" and says why. The rest of the hub (/intent, /run, /gaps, /follow, /keep) still needs the token, so opening the lobby lends strangers nothing but each other. A hub with no token has no guests: there, anyone may meet a bot, as anyone may ask it at /intent.

No centre. There is no chat server, and pyrtan.com runs no hub. Every hub started --chat is a lobby, and a seat that finds nobody at home asks the hubs its hub follows (§3). A hub that joins the default network (pyrtan join) follows a hub in it, and each follow is a way for the people waiting on two hubs to meet; the network has as many lobbies as it has hubs that chat. A person runs one, or opens the Chat tab of any hub that is public.

2. Bots

A bot is one *holder* of trees, picked at random from everyone the hub routes to and is up: its own trees, a service, a trusted member, a hub it follows. A holder whose trees share a word (name, description, tags, triggers) with the seat's interests is picked first, and the introduction suggests a sentence from those trees. A bot's line is produced by rules, in this order:

  1. If the bot's last line was an ask node's question, the line is the answer: the hub resumes the snapshot with it. An ask is a conversation already; chat is where it reads like one.
  2. Otherwise the line is read against the bot's own triggers only (Hub.intent(only=holder)). A match runs there, exactly as /intent would.
  3. A line that reads as nothing (the fillers rule dropped all of it: "hi", "hello pyrtan", "thanks") gets the bot's introduction, and the swarm is asked nothing. Greetings are fillers to every tree in the swarm; making them triggers would change how "hi, what's the weather" reads everywhere.
  4. Otherwise it is asked of the whole swarm, as /intent. When nobody answers, the bot says so with the tree that would, and the sentence is recorded as unanswered like any other. A chat is a stream of real sentences; that is where the next trees come from.

Every bot line carries the outcome's status, tree, peer, output and trace, and the console shows the trace under the line. examples/small-talk answers what is left once greetings are fillers: "how are you", "who are you", "are you a bot", "what can you do", "bye", with one decide on the topic its trigger fixes.

A fix this needed. A trigger may fix inputs (inputs = { topic = "how" }), but /capabilities carried only the patterns, so a hub routing a sentence to another holder dropped them. Capabilities now carry trigger_inputs, and the hub reads triggers with them.

3. Strangers across hubs

A seat that finds nobody here asks up to three of the hubs this one follows, every five seconds while it waits, with a signed Meet {ref, interests}. The followed hub pairs it with one of its own waiting seats or answers met: false. From then on each side's lines go as Say {to, from, text} and the end as Part {to, from}, delivered to the other hub's inbox and signed with the pair's secret, both ways. ref is a second random id per seat, so another hub never learns the id that would let it act as the seat, and it changes on next, so the last stranger cannot reach the next conversation.

The trust rules are federation's. A hub answers a Meet only from a hub that follows it: following is the operator's choice, and a hub that is followed serves its followers. A Say or Part is taken only for a conversation that hub is in. A hub pairs a Meet only with its own seats and never passes one on. Members do not meet across hubs: a member is a browser that chats through its hub's lobby like anyone.

While a seat is asking other hubs it is *busy*, and nobody can pair it meanwhile; two seats that ask each other's hubs at once both get met: false and try again on their next round.

4. Typing (v0.12)

While a person types, the client says so (POST /chat/typing {seat, typing: true}), again every two seconds while they go on, and false when the box empties, loses focus or the line is sent. The partner's poll answers with partner_typing: true and the console shows "Stranger is typing…". While a bot's trees run, its partner reads partner_typing: true too, and the console says "Bot is thinking…".

It is a state, not an event: nothing about it enters the seat's events, so a client that was away sees the conversation and not a history of keystrokes. It goes out by itself five seconds after the last word of it (a closed tab, a lost false), and at once with the next line. A poll that sends partner_typing (what the client is showing) is woken when that is no longer true, both when the partner starts and when the indicator goes out; a poll that does not send it is never woken by typing, so a client that does not show the indicator cannot be made to spin by someone else's keystrokes.

Enabling and disabling. Two switches, both on by default:

Across hubs it is a Typing {to, from, typing} activity, sent on a change and at most every two seconds while typing holds. The sender's hub sends it only for a seat with the indicator on and the receiver's shows it only to a seat with it on, so the rule holds across hubs without either hub knowing the other seat's setting. The outbox and inbox logs leave Typing out: it would push everything else off them. A hub from before v0.12 refuses the activity; the first refusal stops the sending for that conversation.

5. What this design refuses

6. Failure modes, honestly

7. Sequencing