Chats
What a DorkOS chat is, how it stays in sync across tools, and how it works under the hood
Chats
What a chat is
A chat is one back-and-forth with one agent. Every message you send, every file the agent touches, and every reply you get belongs to that chat.
A chat can run on Claude Code, Codex or OpenCode. Every chat lands in the same list, whichever one it uses.
Here's the quick win: start a chat in the DorkOS web app, and if it's a Claude Code chat, you
can also open that exact chat from the claude CLI in the same folder. Same
chat, either window. DorkOS just gives every chat an ID so it can find it again later.
How to work with chats day to day
One turn runs at a time, but every window can queue work. Send a message while an agent is working and DorkOS keeps it in the chat's queue. All connected windows can see the same queue and chat. The next turn starts when the current one has finished and no approval is waiting.
Approvals survive a refresh. If an agent asks to approve running a command or editing a file, that prompt sticks around even if you reload the page, switch chats, or open a second window. It picks up right where it left off.
Chats from other tools show up automatically. Claude Code chats are just files on your
disk, so any tool that can read those files sees the same chats. Start a task from the claude
CLI and it appears in the DorkOS sidebar too, no import step, no sync button.
OpenCode supplies chats from its own store. For Codex, DorkOS lists the threads it has tracked and saves the chat events it observed. This is not a promise that every chat started in the Codex CLI appears in the chat list. If one runtime is unavailable, DorkOS still shows the others and reports which one it missed.
A "New messages" line remembers where you stopped. Come back to a chat that ran on without you and DorkOS marks the spot. Everything you have not seen sits below the line.
That mark belongs to you rather than to one browser, so reading a chat on your laptop moves it on your phone too, with no reload. It is also private: nobody else can see what you have read, and neither can your agents. DorkOS has no "seen by" list and no read receipts. Chats used to keep this mark in a single browser, and that old copy is thrown away the next time you open a chat there, so the line may sit further back than you expect once. Reading the chat sets it straight.
Your own chats stay private from your agents; agent work does not. A chat you have with an agent is yours: in the app, in a direct message with an agent, or from a chat app like Telegram or Slack. DorkOS's agent tools and API don't let your agents list it, open it or search it. An agent's own work is open to every agent, so one can check another: scheduled tasks, messages between agents or chats, and work an extension or a connected app started. Replies in a team channel are open to that channel's agents. Whatever an agent does in your chat still shows in the record; what was said, and which chat it was, stay private. This is not a lock on the files themselves: a program on your computer can still open them, and a request that doesn't say it comes from an agent is treated as you (what this does not cover).
See how full each chat's context is. Every row in the list shows a small gauge of how much of its context window is used, so you can spot which agents are running low before they slow down. A line at the top of the list sums it up, "2 near full · 1 auto-compacted", so you know which one to jump into first. Claude Code chats show this reading even when they're closed; Codex and OpenCode show it once you open the chat, and read "unknown" until then, because those tools only report context usage while a chat is live.
Want a picture of this? A short clip of a chat appearing in the DorkOS sidebar the instant you start it from the CLI says more than the paragraph above ever could.
Reference (for developers)
If you're calling the API directly or debugging DorkOS itself, here's the exact mechanics behind everything above.
Creating and messaging a chat
A chat is created by its first message, sent to a new unique ID with "create": true, along
with a working directory and a permission mode (whether tool calls need your approval). Without
create, a message to an ID the server doesn't know answers 404 and starts nothing. Sending a
message triggers a turn: POST /api/sessions/:id/messages
returns 202 Accepted immediately, and the actual response streams over the chat's durable
event stream, GET /api/sessions/:id/events. That stream carries text deltas, tool calls,
approval requests, and task-progress updates in real time.
The 202 means the server has your message. If the chat was already working, the message waits
on the chat's queue and runs when the current turn ends. queuePosition: 1 cannot distinguish
an immediate start from the first waiting message. Use the stream's turn_start event to know
when execution begins.
One turn at a time
A chat runs one turn at a time, and the server keeps a queue of what is waiting. Anything sent to a chat that is already working joins that queue instead of being refused, whichever window or client sent it. The queue lives on the server, so every client sees the same one, any of them can reword or withdraw a message before it runs, and nothing is lost to a page refresh or a restart.
The X-Client-Id header identifies a client across reconnects and labels the messages it queued.
A write lock still guards a chat so two turns cannot run on it at once, but it is internal now:
it is bound to the turn, released when the turn finishes or errors, and expires after 5 minutes of
inactivity as a backstop.
Who can read a chat over the API
A request that carries an agent token (X-DorkOS-Agent) reads agent work only. GET /api/sessions
and /api/sessions/recent leave a person's own chats out, and every /api/sessions/:id route,
reading or writing, answers 404 SESSION_NOT_FOUND for one, the same answer as a chat that does
not exist. Starting a new chat with create: true is the one thing an agent may do under an id
nobody has used yet. The live list on /api/events leaves a person's own chats out of an agent's
stream too. Without the header you are the owner and read everything, so this keeps your chats
from agents that identify themselves, as DorkOS's own agent tools do, and not from any program
that calls the API without it.
What decides it is what started the chat. You typing in the app, or a chat-app binding, make it yours. A room reply is yours when the room is a direct message between you and an agent, or a chat bridged from Telegram or Slack. In a team channel it is agent work, readable by the agents in that channel. A schedule, another agent or chat, an extension or a connected app make it agent work. A chat with no recorded start counts as yours unless DorkOS knows it was a team-channel reply or a scheduled run.
Storage
History storage differs by runtime:
- Claude Code writes each chat as a transcript file at
~/.claude/projects/{project-slug}/{session-id}.jsonl, in JSONL (one JSON record per line). That file is the single source of truth for the chat. - Codex owns its SDK threads, but its SDK cannot list them or read their history. DorkOS
persists tracked thread bindings and display metadata in
codex_threads, and reconstructs displayed messages from its own persisted event log. - OpenCode keeps chats in its sidecar store, accessed through its SDK.
DorkOS reads the Claude Code transcript to build the chat list, plus per-chat metadata and full message history. Metadata is computed fresh on every request, so it always reflects the current file on disk.
Chat sync
Each connected client subscribes to GET /api/sessions/:id/events. On connect, the server sends a
snapshot: completed history, the in-progress turn, chat status, and any pending approvals. From
there, every client receives the same live events with monotonic (always-increasing) sequence
numbers, including turns triggered by another client or the CLI.
The DorkOS browser uses WebSockets for these streams. On reconnect it sends the full opaque
cursor as ?resume=. HTTP consumers can use SSE and return the same cursor in Last-Event-ID.
The server replays the missing events when it can. If that gap can't be served, the server falls back to
sending a fresh snapshot instead.
The sidebar list stays live the same way: a single global stream, GET /api/events, announces
session_upserted, session_removed, and session_status events as chats are created,
deleted, or change state, across every client and the CLI.
Read state
How far you have read in a chat is stored server-side, in the same table rooms use, keyed by
(user, thread kind, thread id). Two endpoints cover it:
| Method | Path | Purpose |
|---|---|---|
PUT | /api/read-cursors/session/:id | Move your mark to a position. |
GET | /api/read-cursors/session/:id | Read your mark back. |
The stored position counts the messages the server has confirmed, not durable-stream sequence
numbers, so it survives a reconnect and a replay. Both endpoints are people-only: a caller resolved
as an agent gets 403 PEOPLE_ONLY, and no request can name a user other than the caller. A cursor
that actually moves is broadcast as a read_cursor event on the global GET /api/events stream,
which is what updates a second window without polling.
Chat metadata
Most people never need this. Here's where each field on a Claude Code chat comes from, if you do:
| Field | Source |
|---|---|
| ID | The unique ID in the JSONL filename |
| Title | First user message in the transcript |
| Preview | Beginning of the last assistant message |
| Created | File creation timestamp |
| Updated | File modification timestamp |
| Permission mode | Extracted from the SDK's init message |
Working directories
Each chat runs inside a working directory that determines where the agent can read and write
files. It's set when the chat is created, persisted in the URL as the ?dir= query parameter
in standalone mode, and validated against a configurable boundary so the agent can't wander outside
allowed paths. The directory picker in the DorkOS sidebar lets you browse and choose one before you
start a chat.
Opening a chat with the question already written
A link can carry the first message for you. Add ?prompt= to a chat URL and DorkOS opens a new
chat with those words already in the box, ready for you to read, edit, or send:
http://localhost:4242/session?dir=/path/to/project&prompt=what%20changed%20this%20weekThe text has to be URL-encoded — spaces become %20, and most tools will do this for you.
Add &send=1 and DorkOS sends it for you as soon as the chat opens, so the agent is already working
when you look at the screen. It sends once. Refreshing the page, or going back to it, will not send
it again — the link drops both settings from the address the moment they're used.
This is for starting a chat, never for joining one. If the link points at a chat that already has messages in it, the prompt is ignored: nothing is typed and nothing is sent. It is also ignored if you've already started typing, so a link can never overwrite your own words.
Next Steps
Rooms
Talk to one agent or several without picking a project folder first.
Architecture
Understand how chats fit into the overall DorkOS architecture.
Building Integrations
See how chat operations are delivered through the Transport interface, for extension authors and anyone building a custom client.
Tool Approval
Learn how to review and approve tool calls during a chat.