arizuko

arizukoreference › Topics

topics

Schema, creation paths, MCP tools, and resolution order for arizuko topics — the transient work-units overlaid on a group. Narrative introduction lives at concepts/topics.

1. Schema

columns that carry topic state

table.columntypedefaultmeaning
messages.topicTEXT NOT NULL''topic-sessions key, set on inbound and outbound (migration 0008)
chats.sticky_topicTEXTNULLactive-topic pin per chat; NULL / empty = default topic (migration 0016)
chats.sticky_groupTEXTNULLorthogonal — folder pin, not topic (listed only to distinguish)
sessions.(group_folder, topic)TEXT, TEXT''composite PK; one Claude session per (folder, topic) pair (migration 0008)
chat_reply_state.(jid, topic)TEXT, TEXT''last bot reply ID per (chat_jid, topic) — threads outbound on platforms with thread/reply pointers (migration 0014)

Empty-string topic is the default-topic sentinel; NULL in chats.sticky_topic means "no sticky pin set" and is treated as empty when resolving. The store-side normalisation lives at store/groups.go:237 (uses nilIfEmpty on write).

Full table-by-table schema reference: schema.

2. Creation paths

Four sources set a non-empty topic on a message:

inline #name prefix

A message whose body starts with #name (optionally preceded by whitespace) opens topic #name. The prefix is stripped, a synthetic core.Message with Topic = "#" + name is inserted, and the queue fires a turn against that topic.

Regex: ^\s*#(\w[\w-]*)gateway/gateway.go:1746 (rePrefixHash).

Dispatch: gateway.handlePrefixLayer; the #name branch synthesises a message at gateway/gateway.go:1796.

bare # sticky command

A message that is exactly #name (whole-message sticky) updates chats.sticky_topic for the chat. Bare # clears the sticky.

Detection: gateway.isStickyCommand; handler at gateway/gateway.go:1721 calls store.SetStickyTopic and acks with topic → <name> or topic reset to default.

/new and /new #name

/new alone clears the default-topic Claude session for the active folder. /new #name [rest] calls store.DeleteSession(folder, "#name"), then injects "#name " + rest as a fresh inbound, which re-enters handlePrefixLayer on the topic branch.

Command table: gateway/commands.go:37. Handler: cmdNew.

platform-native threads

  • Discord. If the inbound channel is ChannelTypeGuildPublicThread or ChannelTypeGuildPrivateThread, the topic is set to the thread’s channel ID — discd/bot.go:177.
  • Telegram. If msg.MessageThreadID is non-zero (forum-topic supergroup), the topic is set to that integer as a string — teled/bot.go:267.
  • Other adapters currently leave Topic empty; outbound goes into the default topic unless the chat carries a sticky.

Reply-chain inheritance for plain Telegram replies is not implemented as topic carry — reply_to_id threads the visible message, but topic resolution stays sticky-driven. (Behaviour confirmed only for the cases above; other platforms produce empty topic.)

3. Active-topic resolution

At turn-formation time the gateway calls effectiveTopic(chatJid, msgTopic)gateway/gateway.go:1733:

func (g *Gateway) effectiveTopic(chatJid, msgTopic string) string {
    stickyTopic := g.store.GetStickyTopic(chatJid)
    if stickyTopic != "" {
        return stickyTopic
    }
    return msgTopic
}

Precedence:

  1. chats.sticky_topic on the chat row, if non-empty.
  2. The message’s own topic column (from the inline prefix or adapter-set thread ID).
  3. Empty string — the default topic.

The resolved value keys the Claude session lookup at gateway/gateway.go:729 and the multi-topic dispatch loop at gateway/gateway.go:799 (one Claude run per distinct topic per chat-turn batch).

Note. Because sticky beats inline, a message containing #other while the chat is stuck on #current still runs under #current — the # prefix opens a topic via the synthesis path, not by overriding sticky on the current message. Clear sticky with bare # to escape.

4. MCP tools

reset_session

Drops the Claude session for a folder so the next message starts fresh context. Operates on the current resolved topic for that folder — the tool itself takes only groupFolder.

paramtyperequiredmeaning
groupFolderstringyestarget folder

Returns: ok on success, error string otherwise.

Registered: ipc/ipc.go:1047. Authorization: auth.AuthorizeStructural(id, "reset_session", …)auth/policy.go:25.

inspect_session

Returns the current session_id for (folder, topic) plus recent session_log rows. Use to verify a reset took or to see whether a topic has a live session.

paramtyperequireddefaultmeaning
topicstringno''topic to inspect; empty = default topic
limitnumberno10recent session-log rows; clamped to 1–100

Returns: {folder, topic, session_id, recent: […]}.

Registered: ipc/inspect.go:77.

Other topic-adjacent tools: inject_message writes a synthetic inbound (the message’s topic field is derived the same way as a real inbound — sticky and #prefix rules apply on the next turn). No dedicated set_topic / get_topic tool exists today — sticky changes go through the chat-side #name command.

5. Topics and observation

Observed messages are surfaced to every trigger turn alongside the directly-routed messages. The query is folder-scoped, not topic-scoped — see store.ObservedMessagesSince:

SELECT … FROM messages
WHERE chat_jid IN (<jids routed to this folder, minus the firing chat>)
  AND timestamp > ?
  AND is_bot_message = 0 AND content != ''
ORDER BY timestamp ASC
LIMIT 100

The predicate omits topic entirely. A turn that fires in topic #tickets-42 on folder support sees recent inbound from every chat routed to support, no matter what topic those messages carry. Cross-cutting awareness is the design; operators wanting hard isolation should split into separate folders.

Composition into the agent prompt: gateway/gateway.go:728 calls ObservedMessagesSince; :730 hands the observed list to router.FormatMessages alongside the topic-scoped batch.

6. Operator commands

See also: concepts/topics (narrative), concepts/routing (how a chat reaches a folder before topics enter the picture), reference/mcp (every MCP tool).