Part V, The dashboard
29The Chat tab
The dashboard’s Chat tab (#/chat) is a chat whose runs live on the server. Close the browser in the middle of an answer, open the chat on a phone, and it shows the answer at the token the engine is writing now and keeps streaming. The tab is in beta in this release; a Beta pill beside it says so, and Section 29.6 lists what does not work yet. docs/chat.md has the full contract and every route.
29.1 What runs where
The browser talks to the engine server on port 8000, which passes /v1/chat-app/* to chatd after the dashboard’s own auth check. chatd (python -m chatd, on 127.0.0.1:8016) is a standard-library Python process. It holds the stream to the engine, so a run does not end when a browser closes, and it writes every change of a conversation to an event log with a sequence number. A device that attaches gets a snapshot and then follows the log from there. chatd reaches the model as any client does, through POST /v1/chat/completions, and reads what the model can do from its card in /v1/models, never from its name.
chatd starts with the engine (tandem start, ops/start.sh) and stops with tandem stop; tandem restart and every engine restart leave it running, and the watchdog starts a chatd that died. QSE_CHATD=off in run/local.env opts out.
~/TandemLLM/bin/tandem chat status # pid, interpreter, data folder, URL
~/TandemLLM/bin/tandem chat start | stop | restart | logschatd runs under the install’s venv and reads the engine’s secrets file (QSE_SECRETS). When chatd is down the tab says so, with the command to start it, and checks again by itself; the in-browser mock loads only for ?chat=mock.
At most two runs talk to the engine at once (QSE_CHAT_MAX_RUNS); the others wait as queued. A run that waits for a permission prompt gives its slot back until the answer comes. A run on a cloud model takes no slot: its calls run at the provider, not on the box. An engine restart does not touch chatd: a run that loses the engine retries for 10 minutes, then ends interrupted with its text kept, and Continue resumes it. A call the engine refuses for memory is retried once, then the chat is compacted and the smaller prompt sent; from then on the chat compacts at 80 % of the refused size as well as of the window.
File texts stay out of the request: a finished write_file’s content, and an edit_file’s long old or new text, reach the model as a stub naming the path, size, hash and the read_file call that shows it; long read_file and bash results stay whole until a compaction and are stubs after it, so a request changes only at its tail and the engine’s prefix cache holds the rest. A compaction lands under half the limit that set it off, keeping fewer steps of a long run when it must. render_page says which graphics APIs the check browser has: WebGL2, no WebGPU, so a page’s WebGPU path is not checked.
Since 0.12.2 one step’s tool results share the room the model has left: half of what is left under the compaction line, 2,000 tokens at least, split across the step’s calls. read_file stops at whole lines and ends with the offset to read on from, and a line longer than one read goes on with its column argument. Before 0.12.2 a 700 KB file came back as its first and last 16,000 characters, with the rest left out unsaid. Each request’s estimate carries a margin for the text the engine has not counted yet. Near the window chatd counts the request on our engine (POST /v1/tokenize) and refuses one that is over before it sends it.
29.2 The sandbox
Each chat’s tools run in a container of its own, tandem-chat-<id>, from the image tandem-sandbox:1. Every container runs as uid 1000 with no capabilities, a read-only root and no network of its own, at most 1 GiB of memory, 4 CPUs and 512 processes, and no GPU. Its internet goes through chatd’s per-chat proxy, which refuses the LAN and the box itself. A chat’s tool call gets its sandbox: when memory is short, the engine gives back what it holds idle first. A call waits for its sandbox at most 60 seconds and then fails with the reason, which the model reads and the chat shows on the call; a process killed at the memory limit fails its call with “out of memory (1 GiB sandbox limit)”. The memory guard stops the least recently used idle sandbox only when the box itself runs out, and stops any sandbox that sat idle for 30 minutes. The chat’s files stay.
The port preview shows an app that listens in the sandbox. Since 0.12.2 an app’s answer goes on to the browser once its head is read, so an event stream or a long-poll shows as it arrives. A page, script or style sheet up to 8 MiB is read whole and rewritten; a bigger one passes as it is, with X-Tandem-Rewrite: skipped. The app has 300 s to start its answer. A browser that leaves ends the relay in the sandbox, and the idle stop can stop it again.
A deleted chat goes to the trash for 7 days, and its sandbox stops at once. POST /v1/chat-app/conversations/<id>/restore brings the chat back, and its sandbox starts again on its next tool call. When the 7 days end, or on DELETE /v1/chat-app/conversations/<id>?forever=1, chatd removes the container and the chat’s folders under workspaces/, homes/, egress/ and snapshots/. At its start chatd also removes every container whose chat is gone or in the trash, and logs each one.
29.3 Permission modes
Each chat has a permission mode, and the Tools settings page holds the default that a new chat gets. A pill in the composer’s options row shows the chat’s mode and opens a menu of the three, with one line each; YOLO is orange. A change applies from the next tool call. The modes came from a test run on a phone on 2026-10-13: six bash prompts for commands such as grep and cp took 20 s of tapping, and every one of those commands ran inside the chat’s own sandbox.
| mode | what asks first |
|---|---|
| Ask | every risky call, as before 0.8.2 |
| Auto (default) | only a call that leaves the sandbox: web_fetch to a domain this chat has not fetched yet, an MCP tool its server does not mark readOnlyHint, an imported Open WebUI tool whose file imports a network or process module, and rm or rmdir aimed at /, /workspace or a home directory. Bash, files in /workspace and render_page run without a prompt. |
| YOLO | nothing |
Some limits hold in every mode. A deny rule refuses the call, and so does a tool set to “never”; a tool set to “ask” asks. Plan mode offers only the read tools. The sandbox stays as locked down as before.
A call that ran without a prompt still leaves a record. Its tool card carries a small badge that names what allowed it (“auto”, “yolo”, “always”), and tool.started carries approved: "auto", "yolo", "rule" for a saved rule, or "user" after a prompt. The chat’s event log gets a tool.auto_approved event with the call’s one-line summary, and the run’s mode is on run.started and on the assistant message. The default and each chat’s mode have routes of their own (GET and PUT /v1/chat-app/permissions/mode, and /v1/chat-app/conversations/<id>/permission-mode). A chat from before 0.8.2 has no mode and runs in the default.
29.4 Rules and “Always allow”
A bash rule names a command’s first word, or its first two for programs with subcommands (git push, npm install); grep tilt saves grep. A chained command (&&, ||, ;, |) runs without a prompt when every part has an allow rule, and is refused when any part hits a deny rule. “Always allow” on such a command saves a rule for every part no rule covers yet, and the prompt names them: “Always allow cd, node, echo, cat”. Until 0.8.2 it offered only the first word, which never covered the whole command, so the prompt came back: one chat saved cd 11 times for cd /workspace && node ... > run.log 2>&1; echo "exit:$?"; cat run.log. Store schema 6 keeps one copy of each duplicate rule. A command with $(...) or backticks matches no rule and offers no “Always”. All of this matters in Ask mode; Auto runs bash in the sandbox without asking.
29.5 Agents mode and workers
A chat setting, off by default. With it on, the main agent talks with the user and plans, and a task call gives the hands-on work to a worker: a fresh assistant that works in the chat’s sandbox and /workspace and sees only its brief, the todo list and the user’s message. The file text lives in the worker’s context and goes with it; the main agent reads only the worker’s report and chatd’s check of it, at most 8,000 characters. One long loop of 2026-10-17 had grown from 25k to 162k tokens in about 50 minutes with every file it wrote and read in its context.
The main agent reads files in pieces of at most 200 lines and writes none: it has no bash, write_file or edit_file. A brief names its Goal, Files, Constraints and Check and holds no code, or chatd refuses it: the main agent says what a file must do and how to check it, and the worker writes it. The worker also gets the user’s request word for word, after the brief. A worker runs in the chat’s run slot with the chat’s permission mode, makes at most 40 model calls, and ends with a report in a fixed shape; chatd checks it against the snapshot diff and the calls that really ran. Stop stops both, and Continue resumes an interrupted worker before its parent. The Task card in the chat shows the worker’s run and its report.
29.6 Known limitations
The agent loop does not yet keep its prompts prefix-stable from step to step (SRV-375), and old tool results and old thinking stay in the context until a compaction (SRV-365). A compaction writes a general summary, not the task’s state (SRV-373). Nothing notices when the model repeats the same failing approach with different calls (SRV-367), long tool output has no paging (SRV-371), and searches run in the main context with no sub-agents (SRV-381). The engine-side limits of 0.8.0 still hold (Section 38.19); the phone layout’s keyboard behaviour is still not verified in Safari on an iPhone.