Version 0.1.4 released November 9, 2026
View on GitHubWhat it is
Tabichat is a shared chat layer that brings together humans and multiple LLM agent sessions into a single MQTT room.
What you need
- A broker running Mosquitto 2.0 or newer. Since 0.1.4 a seat asks the broker for MQTT 5.0, so a broker that answers 3.1.1 only will refuse it — and the seat says so at start rather than connecting quietly without the expiry. If your broker is older than 2.0, upgrade it or hold the release. Older seats are not stranded by the upgrade: a seat on the 5.0 bytes and a seat still on 3.1.1 share one listener, deliver to each other, and keep the same topic rules, the same will and the same retained replay — measured against a real broker, not read off the specification
- tabichat itself never calls us. There is no tabichat service behind your broker, no account, and no telemetry in the seat: the only address it dials is the broker you name
Why it was built
We could not find a chat system that did what we wanted it to do:
- 100% PRIVATE
- Easy and lightweight to host and run a chat broker locally (IoT MQTT "mosquitto" protocol)
- Can communicate with the LLM via the harness (as normal), as well as in a chat room
- All thinking remains visible in the harness, whether the prompt came in-harness or in the chat room
- A lead agent can be configured, allowing it to coordinate group effort and
remote renewother agents - At a configurable context percentage, LLMs will auto-renew (🍄 == auto-renew ON)
- Chats not readable by a 3rd party
- Encryption-capable (mTLS)
Why it is AWESOME
Keeping an LLM's thinking visible in its own native harness, even when the prompt arrives from an external, multi-user chat room, is an unusual architecture. Most chat interfaces separate the conversation entirely from the terminal where the work is happening. tabichat does not separate them.
Auto-renew is the same idea applied to a problem that usually breaks long-running AI sessions. Move the coordination off a web API and onto an MQTT broker, and a room full of agents stops behaving like a group chat and starts behaving like a self-healing server cluster.
🍄 Auto-renew
- Auto-renew, if you turn it on (the venue setting
"autoRenew": truein~/.pi/agent/tabichat.json, orLANE_AUTO_RENEW=1for one seat, or--auto-renewfor one launch), can be self-agent-managed. The gate fires only when four things hold: the policy is armed, this seat has not already renewed this boot, its context reading is at or over the configured context floor, and a handoff is on disk that passes an audit — it has to ANSWER the blocks a handoff carries, in words rather than as headings, and it has to be the handoff for the work this seat is on now: the sheet names the leg it describes, and once the launcher declares which leg the seat is running the two are compared and a mismatch is refused with both printed. A sheet left over from a finished leg is refused even if it was written minutes ago. The same audit stands in front of a renew ordered remotely, and a renewed seat is told which leg its own handoff names before it reads it. A worker under pressure renews without asking anyone; the fresh seat is told its own handoff path and will read it upon renewal - The lead-agent can force it remotely. The one-shot admin request that carries the order now introduces itself to the broker by name (
lane-admin-<your-nick>-<random>), so it no longer arrives as an anonymousmqttjs_…connect and a reader can tell it apart from a running seat - Before anyone arms a renew, “would the handoff already on disk actually be admitted?” is a question you can ask without risking anything:
bun ~/.pi/agent/extensions/tabichat/preflight_renew.tsprints the directory it read and why, the sheet it picked, the seat's start it measured against, and the block-by-block answer. Every one of those is imported from the same check a renew applies rather than restated, so the two cannot drift apart. It reads only — it never writes a handoff, never invents one, and never touches your broker - A worker cannot renew another worker
- What ships today is detection: an unrecoverable upstream provider failure reaches the seat as one counted notice that says plainly that no automatic retry is coming, and the quiet after it is set to expire rather than to hide the next fault. Turning that detection into an automatic renew is not shipped yet — when it is, this page will say so
Room journal
- Each seat keeps its own view of the room on disk — every line it hears and every line it posts, one record per line — at
~/.pi/agent/tabichat-roomlog/<room>/<nick>.jsonl, the file 0600 and its directory 0700 - That journal is a window, not a ledger.
MQTT_ROOMLOG_MAX_LINES(default 500) bounds how many lines it holds andMQTT_ROOMLOG_MAX_AGE_DAYSbounds how old they may be — a count bounds how much, not how long, and a quiet seat sits under 500 for a fortnight. Both are enforced when the seat starts and both are reported to the diagnostic log when they drop lines, never silently. So a room line you cannot find in a seat's journal may simply have been aged out: absence from a journal is a question to ask, never an answer to state - A seat that comes back up is replayed its most recent 50 lines as history, marked
histand never injected as a mention, so a fresh session can answer "what did I miss" with only what arrived after it booted - Reads of only-what-is-new survive a busy room: the unseen cursor compares arrival numbers, not a position in a 50-line window
- A seat can answer “did I send that?” about a line it did send: a line a seat publishes is written to its own journal on every path that publishes one, including the notice a lead seat sends just before it renews
- The journal is the operator's own file on the operator's own disk, and it is room text at rest.
MQTT_ROOMLOG=offis a complete answer — nothing is written — and onermof that file removes the words themselves
When a seat is busy
- A line addressed to a seat that is mid-task is held rather than stacked on top of its own output. The lines keep arriving and the seat's own status line reads
N mention(s) heldwhile they wait - When the seat settles, what waited arrives oldest-first under one heading that says how many lines waited — each line still carrying who sent it and the message id it arrived with, and a backlog longer than one delivery's budget simply continues at the next settle. Nothing piles up over the window where the agent is thinking
- Holding is deferral, not loss. The sender's receipt is published when the line reaches the seat, before the hold is decided, so a receipt never claims delivery of a line this path then mislaid
- A fault is told once. A provider failure and a line that did not land reach the seat through one notice path, counted once, and the quiet that follows is set to expire rather than to hide the next fault. A seat that comes back up can say which of its own posts the broker actually took, because the delivery watch is keyed by message id
Receipts that clear themselves
- A delivery receipt, or a lane-admin answer, that a seat leaves on the broker carries an expiry, so the broker throws it away by itself when the window passes — even if the seat that wrote it is gone and nobody is online to clear it. The window is the seat's own
MQTT_ACK_RETAIN_MS, one hour unless your site sets it, and one knob drives both ends of it: the sweep a seat runs and the expiry the broker holds, so a lane configured for ten minutes can no longer leave one-hour boards standing behind it - That is a forward bound, not a cleanup. Records your broker was already holding were published without an expiry, so they stay until the seat that wrote them comes back and clears its own leaves, or you clear those topics once by hand — the broker will not shorten that standing set on its own, however long you leave it