Chats and turns
A chat is one CLI session with a thread. ChatCore keeps a ChatSession per chat; the session owns the CLI process, turns what it writes into thread items, and holds the queue and the requests that wait. The requests below are those of the protocol; host.chats has a method for each.
Creating and attaching
chat.create makes a chat, or loads it from disk. A new chat runs on Claude Code unless it names a provider, works in cwd (else the environment's HOME, else the user's home), starts on the catalog's default model, under the CLI's default account or the one last picked, and in full-access unless it names a runtimeMode. Name all of them in an app. A chat loaded from disk keeps the provider, folder, account and model it has; change those with chat.configure, which admit and runtimeModeFor look at again.
Creating a chat starts nothing. The CLI starts on the first message and stays up between turns; a CLI that went down is started again with its session on the next message. chat.attach answers the thread and streams its events to that client from then on.
Sending
chat.send answers { queued, turnId }. A message sent while a turn runs waits in info.queue and goes out once the turn settles. chat.unqueue takes a message back out, answering it so it can be edited, or request-not-found when it already went. chat.sendNow moves a queued message to the front and stops the running turn, so it goes out next. Stopping a turn can pause the queue (queuePaused), and a turn that stopped on the usage limit holds it too: read the info before counting on the rest going out.
Deltas are joined before they go out, so a client gets fewer, larger events than the CLI writes; the thread on the host always holds the whole text. chat.status goes to every client with the chat's info, including a short form of the requests that wait. A turn is running, done, aborted or error; info.running is only whether the process lives, and an idle chat can still have a shell or a subagent of its CLI running in the background.
Approvals and questions
A pending approval or question is an item with a requestId. chat.approve answers an approval with allow, allow-always or deny, and a reason for a CLI that takes one. allow-always grants the rule the CLI proposed, nothing wider. chat.answer answers a question by question id, and chat.dismiss leaves an async question by its item id. A request settled elsewhere first, by another client or in the CLI's own prompt, is refused with request-not-found.
Stopping a turn settles the requests it left open, so no approval stays pending under a finished turn.
Subagents
chat.subagent reads the conversation of an agent the chat delegated to, from the CLI's own transcript, a page at a time, and with watch: true sends chat.subagentChanged while it grows. chat.stopSubagent stops a subagent a task of your app opened; a subagent of the CLI's own can only be marked stopped. chat.stopTask stops one background shell or monitor, on a CLI that can.
Visuals
A visual is a page an agent publishes in a chat. Agents publish through a command of your app, which calls host.chats.publishVisual(chatId, { title, html, maxHeight?, heights?, turnId? }) and gets the ChatVisual back. The chat may be one nobody loaded; a visual published while a turn runs belongs to that turn unless the input names another. listVisuals(chatId) answers a chat's visuals in the order they were published, and chat.removeVisual takes one away.
VisualStore keeps them: the list in chats/<id>.visuals.json, each page as <id>.html in the chat's attachment folder, so host.chats.attachment(chatId, visualId) finds a page the way it finds an attached file, with the mime type text/html. Publishing puts the bootstrap of injectVisualBootstrap in the page and checks VISUAL_LIMITS; a page that breaks one is refused with visual-invalid or visual-too-large, with a message that tells the agent what to change. Every change goes to the clients attached to the chat as chat.visuals, and chat.attach answers the list too.
A clear and chat.kill take a chat's visuals along. A core of your own that forks chats gives the fork its visuals with visuals.copyChat(fromChatId, toChatId, keep?), which writes each page again under the fork, so removing either chat leaves the other's pages. removeChat(chatId) takes back what a fork that failed wrote. Never serve an attached text/html file on your app's own origin; see Serving a page.
Stopping and removing
| Request | |
|---|---|
chat.cancel | Stops the turn and keeps the conversation. With subagents, also ends the agents the chat opened. |
chat.clear | Starts the chat over. Refused with chat-busy during a turn, unless force. |
chat.compact | Folds the context, natively or by prompt, as the CLI's compaction capability says. |
chat.kill | Removes the chat: its record, log, attachments, bookmarks and visuals, and ends its CLI. |
| Detaching or disconnecting | Stops streaming to that client. The chat goes on. |
host.close(), ChatCore.shutdown() | Writes every thread and ends every CLI, and settles once they exited. |
A CLI is ended by closing its input, then, after three seconds, SIGTERM to its process group and SIGKILL two seconds later. chat.kill does not wait for that; shutdown does. Process groups are a POSIX notion: check how ending works on the platforms you ship.
Restarts
A host that goes down during a turn freezes it before it writes the thread, so the turn does not read as failed. When the chat loads again, the turn is ended as aborted with a note written by notResumedNote, unless onInterruptedRun answers true: then the host owes a resume and calls recoverInterrupted() and resumeRun(chatId, turnId, attempt) when it is ready. A turn is taken up on at most two processes. endedAt, unownedReason and resumeWords let a host say when a chat was stopped on purpose and how the resume is worded.
A turn that hit the plan's usage limit or an overloaded model ends as an error turn with a limit. Taking it up again when the limit lifts needs limitResume on the core; AgentHost installs none. resumeAtReset on a chat turns it off for that chat. Moving the chat to another account is a person's choice, never automatic.