arizuko

arizuko › concepts › topics

topics

A topic keeps two conversations inside one folder apart. A topic is a label on a message that splits the chat of one folder into separate threads. Each thread has its own session and its own reply chain, so messages in #support do not see messages in #billing.

A topic comes from one of three places

routd reads the topic from one of three places:

Threads of the platform
The MessageThreadID of Telegram, the thread channel ID of Discord and similar fields go into Message.Topic when the adapter gives the message to routd.
The web channel
A web message carries a topic slug. The chat widget chooses it before the request goes out.
The #topic prefix
When a user starts a message with #name, routd removes the prefix and runs the rest under the topic #name. routd reads the prefix from one message per batch: the newest one, or the oldest slash command if the batch has one. So routd ignores a #name on an earlier message in the same batch.

Each topic has its own session

Each pair of a folder and a topic has one row in sessions. The row holds the id of a Claude Code transcript in the folder. The figure shows what happens to that id on every turn:

┌──────────────┐    ┌──────────────────────────────────────────────────┐
│ chat message ├───►│ (folder, topic): the platform thread, a route    │
└──────────────┘    │ pin, a sticky #topic, or the web chat's slug     │
                    └─────────────────────────┬────────────────────────┘
                                              ▼
                    ┌──────────────────────────────────────────────────┐
                    │ first turn in a topic: EnsureTopicLineage writes │
                    │ the sessions row: parent_topic, forked_at, the   │
                    │ parent's observed_cursor                         │
                    │ copyParentSession: the parent's jsonl under a    │
                    │ new id; no parent file ► the cursor is cleared   │
                    └─────────────────────────┬────────────────────────┘
                                              ▼
                    ┌──────────────────────────────────────────────────┐
                    │ idle past SESSION_IDLE_EXPIRY ► routd drops it   │
                    │ no id ► routd mints one and runs PutSession      │
                    │ BEFORE it calls runed                            │
                    └─────────────────────────┬────────────────────────┘
                                              │ RunRequest.SessionID
                                              ▼
                    ┌──────────────────────────────────────────────────┐
                    │ guest: the harness resumes                       │
                    │ ~/.claude/projects/-home-node/<id>.jsonl         │
                    └─────────────────────────┬────────────────────────┘
                                              │ submit_turn
                                              ▼
                    ┌──────────────────────────────────────────────────┐
                    │ recordTurnResult: PutSession with the id the     │
                    │ harness ran under; a killed turn reports none,   │
                    │ and the next turn resumes the id routd minted    │
                    └──────────────────────────────────────────────────┘

routd records the id before the guest starts and never learns it from the guest afterwards. So a turn that a deploy or a deadline stops leaves a transcript, and the next turn continues it. A chat that is idle for two days, the default of SESSION_IDLE_EXPIRY, starts a new session.

A topic isolates the session and the reply chain

The agent keeps one Claude session per pair of folder and topic. So /new #support resets the support thread and changes nothing else in the folder. The store also records the last bot message per topic, so replies on the platform attach to the correct message.

For the web channel, routd groups inbound messages by topic and runs one agent per topic, one after another.

A fork copies the parent transcript

A fork copies the Claude Code session file of the parent topic to a new session UUID. The child resumes from the end of the parent. Every earlier turn is already in the history of the child, because the child got the real session file and not a summary. The sessions row records parent_topic and forked_at for audit.

Three triggers fork a topic, and all three copy the transcript of the parent in the same way:

The default fork from main
The first turn in a new topic forks the main session of the folder.
A reply to a parent
When the trigger message is a reply, the parent is the topic of the replied-to message, not main. A reply inside #deploy stays in #deploy. A reply from a new thread to a #triage message forks from #triage.
The fork_topic MCP tool
The agent starts a focused side conversation. With force=true, the tool overwrites an existing child.

On every turn, the agent gets a <topic name="X" /> envelope, so it knows its scope. No <inherited> block exists, because the history of the parent is already in the child session.

sessions.observed_cursor is also per topic. So two topics in the same folder both see the ambient <observed> messages, and one topic does not use them up for the other.

Forks have two limits. The copy is a Claude Code transcript, so codex and pi start the child with no history. Every fork is a full copy of the parent file, and nothing deletes the copies, so .claude/projects/ in a busy folder only grows.

A user can pin a chat to a topic

A user can pin a chat to a topic with a message that contains only #support. routd stores the pin in the sticky_topic field of the chats row. Every later message in that chat runs under #support until someone sends a bare # to clear it. A pinned topic wins over the thread ID from the platform.

Routing picks the folder before the topic

Topics work inside one folder. routing picks the folder first, and then the topic applies. scopes explains how folders relate and what counts as ambient context. a turn, end to end shows where the session id sits in the turn. [ROUTING.md] has the resolution order and the fork details.