Gateway

Packages: gateway/, queue/, router/. Entrypoint: gated/.

Tables owned: routes, registered_groups, router_state, sessions, session_log, system_messages.

Message loop

The gateway polls store.NewMessages every 2 seconds using a timestamp cursor persisted in router_state. On each tick it also calls store.ActiveWebJIDs to pick up web-channel messages that arrived since the last agent run. The cursor advances after each batch is dispatched.

Messages pass through impulseGate before dispatch. The gate assigns weights by verb: social verbs (reactions, typing) weight 0; all others weight 100. A JID is held until its accumulated weight reaches the threshold or a 5-minute maximum hold elapses. This batches rapid-fire messages into a single agent invocation.

Three-layer pipeline

Every inbound message flows through four ordered layers. Each layer owns its namespace and does not consult the next:

LayerImplementationFunction
1. StickyCode (in-memory)Absorbs bare @name/#topic tokens as routing state updates
2. CommandCode (gatewayCommands table)Matches slash-prefixed first token (e.g. /new, /ping)
3. PrefixCode (no DB)Inspects content for inline @name/#name tokens, navigates to child folder or synthesizes topic session
4. RoutingData (routes table)Walks rows in seq order, returns first matching target

Commands never touch routes. Prefixes never touch routes. Only the routing layer reads the routes table.

Routes table (v0.25.0)

CREATE TABLE routes (
  id             INTEGER PRIMARY KEY AUTOINCREMENT,
  seq            INTEGER NOT NULL DEFAULT 0,
  match          TEXT    NOT NULL DEFAULT '',
  target         TEXT    NOT NULL,
  impulse_config TEXT
);
CREATE INDEX idx_routes_seq ON routes(seq);

No jid column. No type column. One table for the whole instance.

Match expression language

match is a space-separated list of key=glob pairs. All pairs must match for the row to fire. Empty match = wildcard (matches everything).

KeyResolves to
platformPlatform name (e.g. telegram, discord)
roomPost-colon portion of JID (e.g. -5075870332)
chat_jidFull JID (e.g. telegram:-5075870332)
senderSender name or ID
verbMessage verb (defaults to message)

Globs use Go path.Match semantics: *, ?, [abc]. No regex. Case-sensitive.

Example routes

seqmatchtarget
0room=-5075870332krons/content
10platform=telegram verb=mentionrhias/mentions
20verb=followkrons/notifs
30platform=blueskybsky/feed
99default/firehose

Target convention

TargetRouted to
krons/contentFolder (agent container)
folder:krons/contentSame (folder: prefix optional)
daemon:onbodHTTP POST to registered daemon (future)
builtin:pingIn-gateway handler (future)

Commands

Gateway commands are intercepted before agent dispatch and are not forwarded to the container. Registered in gateway/commands.go as a single table.

CommandAction
/newReset session for the current group/topic
/new #topicReset session for a named topic only
/pingReply with pong (liveness check)
/chatidReply with the current JID
/stopEvict session cursor, stop active container for group
/statusRoute to dashd for status reply
/approveHTTP POST to onbod (onboarding only)
/rejectHTTP POST to onbod (onboarding only)

Prefix dispatch

Before route resolution, the gateway inspects message content for prefix forms:

These override normal routing. The prefix is stripped from the message before forwarding to the agent.

Unified message path

All messages (inbound, outbound, delegation, escalation, topic routing) flow through PutMessage to the messages table. No separate outbound path.

Previously (v0.24.2): three disjoint paths:

Now (v0.25.0): single path via PutMessage. Delegation is durable by construction (prompt in SQLite before queue sees it).

Job queue

Package: queue/. Each group has its own serialized queue (GroupQueue). A global cap limits total concurrent container invocations (default 5).

Output processing

Container stdout is scanned for the delimiter pair:

---ARIZUKO_OUTPUT_START---
{"status":"ok","result":"...","newSessionId":"...","error":""}
---ARIZUKO_OUTPUT_END---

Everything outside the delimiters is discarded. The gateway strips <internal> XML tags from result before sending to the channel adapter. <think> blocks are also stripped.

The status field controls session advancement: on error with no output the cursor rolls back (messages retry on next poll); on error with output the cursor advances (partial work preserved).

Session management

Sessions are stored in store.sessions keyed by (group_folder, topic). The session ID is the Claude Code session identifier returned in container output. On /new or /new #topic, the session row is deleted; the next container invocation starts a fresh session.

Session history is written to session_log with start time, end time, result, and error. On container error with no output, the session is evicted so the next invocation retries from the same message cursor position.

impulseGate weight batching

The gate accumulates messages per JID. When the total weight of pending messages for a JID exceeds the configured threshold (default 100), the batch is released for processing. Messages held longer than 5 minutes are flushed regardless of weight. This prevents a burst of low-weight events (e.g. reactions) from triggering a container invocation each.

Web channel

Web messages use web:<folder> JIDs. The gateway discovers active web JIDs via store.ActiveWebJIDs(since) on each poll tick. processWebTopics splits messages by topic and runs one agent per topic in parallel, subject to the global queue cap.