Files
iris_x_hermes/docs/06-channels-cron-search.md
T
ARIA 7a6d922d12
CI / Kotlin tests (android host + desktop) (push) Successful in 8m5s
CI / Gateway plugin tests (push) Successful in 9m47s
Add QR pairing (terminal QR, in-app scanner, iris://pair deep link)
2026-08-22 22:43:13 +02:00

8.3 KiB

06 — Channels, Threads, Cron Delivery, Search

6.1 Concept model

The hermes gateway already models conversations as SessionSource with chat_id + thread_id + chat_topic (gateway/platforms/base.py:7047 build_source). We map app concepts onto these existing primitives — no new gateway identity concepts.

App concept hermes primitive Example
Default chat home channel chat_id default
A thread (inside default chat) thread_id under the default chat_id chat_id=default, thread_id=t_12
A user-created channel a new chat_id chan_7
A thread inside a channel thread_id under that chat_id chat_id=chan_7, thread_id=t_31
  • chat_id = the conversation lane (a channel or the default chat).
  • thread_id = an optional sub-lane within a chat_id (topic-like).
  • The channel directory (plugin SQLite, channels.db) stores {chat_id, name, kind: default|channel|thread, parent_chat_id, is_default, created} and is the source of truth for the app's channel list.

6.2 Default chat

  • On first connect, the plugin ensures a default channel exists: chat_id = ANDROID_HOME_CHANNEL (default default), kind=default, is_default=true, name "Default".
  • It is also the cron home channel (cron_deliver_env_var= ANDROID_HOME_CHANNEL), so deliver=android (bare) routes here.
  • The app opens the default chat on launch.

6.3 Threads (toggle for overview)

  • Requirement: a default chat where the user can activate threads for a better overview, or not.
  • Thread toggle (per default chat, in the chat header menu):
    • Threads OFF — flat conversation; all messages use thread_id=null.
    • Threads ON — the app groups the conversation into topic-like lanes. Each new "topic" mints a thread_id (via channel.create {kind:thread, parent_chat_id:default} or an implicit thread). The UI shows a topic switcher (like Telegram topics) above the message list.
  • Threads are app-organized but gateway-real: each thread_id is a distinct hermes session lane, so context is isolated per thread and cron can target a specific thread.
  • The gateway's create_handoff_thread is used where hermes wants to open a named thread (e.g. continuable cron).
  • Threading is only active on the default channel. The topic switcher, the "new topic" affordance (Ctrl+T), and auto-threading all apply to the default channel only; user channels stay flat (the gateway also ignores auto_thread for non-default channels).

6.3.1 Auto-threading (Telegram topic-mode workflow)

With Threads ON, a message sent in a channel's flat lane (no active thread) gets its own fresh thread, the way Telegram topic mode mints a topic per new conversation — and the AI names it instead of the user:

  1. The app sends message.send {…, auto_thread:true} (only from a flat lane, with non-empty text, never for slash commands).
  2. The gateway mints a thread (channel.create-equivalent, kind:thread) under the channel, named instantly from the user's opening message via hermes' session-title derivation (agent/title_generator.derive_title — a deterministic slice of the user's own words, no model call), and broadcasts channel.created {auto:true}.
  3. The user echo, the agent turn, and all streaming frames carry the new thread_id — the whole conversation lives in the thread.
  4. In the background, the gateway upgrades the name with the model's title (agent/title_generator.generate_title, the title_generation auxiliary task) and broadcasts channel.renamed. This is hermes' two-stage session titling (derived < llm < user) applied to the thread name; failures leave the derived name in place.

App side: on channel.created {auto:true} under the flat lane it is viewing, the app jumps into the new thread and relocates the optimistic pending bubble from the flat lane into it (the echo arrives in the thread lane). Follow-ups sent inside the thread stay there; the next flat-lane message starts another thread. Media-only sends and slash commands stay in the flat lane (nothing to title / session-scoped, not conversation starters).

6.4 User-created channels (for cron delegation)

  • Requirement: the user creates new channels so cron job outputs can be delegated to them instead of the default chat.
  • channel.create {name, kind:"channel"} → plugin mints chat_id = chan_<n>, stores in directory, broadcasts channel.created to all devices. The new channel appears in the channel list.
  • channel.rename / channel.set_default / channel.delete manage the directory (rename broadcasts channel.renamed; delete is a hard delete -- the channel/thread row is removed and the lane's history is wiped from the outbox and the hermes session store, so nothing is recoverable and no search trace survives).
  • Automation channels: a channel can be marked automation (channel.set_automation {on}, long-press / right-click menu; the default channel cannot be marked). Automation channels are read-only for the user: they exist only to receive gateway-originated output (cron job output, webhook output). The app replaces the composer with a read-only notice, and the gateway rejects message.send into them (error unsupported). The flag is server-authoritative (synced to all devices via the channel.renamed full-entry response) and shown as a gear badge in the channel list.
  • App affordance for threads: long-press (touch) / right-click (desktop) a topic chip → "Rename" (channel.rename) or "Delete" (channel.delete). Deleting the open thread falls the app back to the channel's flat lane.
  • Cron targeting (the key payoff): because the plugin registers parse_target_ref_fn and cron_deliver_env_var, cron jobs and the send_message tool can target any channel/thread:
    • deliver="iris" → home (default) channel.
    • deliver="iris:chan_7" → that channel.
    • deliver="iris:chan_7:t_31" → that channel's thread.
    • In-chat: the agent's cronjob tool can be told "deliver to the Cron Reports channel"; the gateway resolves the name via the channel directory.
  • In-app affordance: each channel's menu has "Set as cron target" / shows a badge when a cron job points at it, and the channel name is offered in the /cron creation flow.

6.5 Cron delivery mechanics (how it works under the hood)

  • Cron resolves delivery targets in cron/scheduler.py:2148 (_resolve_single_delivery_target). For platform:chat_id[:thread_id] it calls tools.send_message_tool.resolve_send_target, which uses our parse_target_ref_fn to parse iris:<chat>[:<thread>].
  • Delivery then calls the live adapter's send(chat_id, text, …) (gateway running) → our WS message frame (or outbox+push if the app is offline).
  • Cron deliveries are framed with a [Cron delivery: <name>] header by hermes; the app can style cron messages distinctly (e.g. a small "⏰ " chip) using the role:"cron" / header.
  • Mirror option: hermes cron.mirror_delivery (default off) can also mirror a cron delivery into the origin session; we leave it off to keep channels clean.
  • Requirement: search with settings "search everywhere" / "search in this chat/channel".
  • Backend: hermes's session store is SQLite + FTS5 (hermes_state.py, hermes_state_search.py). The plugin's search.py opens the session DB read-only and runs FTS5 queries.
  • search frame → search.results:
    • scope:"all" — search everywhere (all channels/threads/sessions).
    • scope:"chat" — restrict to the given chat_id (and optional thread_id).
  • Result hit: {message_id, chat_id, thread_id, role, snippet, ts}. The app renders a results list; tapping a hit navigates to that channel/thread and scrolls to + highlights the message.
  • Query syntax: plain text (FTS5). Optional role: / channel: qualifiers are a nice-to-have; v1 is plain-text + scope.
  • Privacy: search is local to the user's own hermes home; no data leaves the machine.

6.7 Channel list frame

hello.ack and channel.* frames carry the directory. App keeps a local copy (Room) and reconciles on channel.* events (merge, don't clobber — see 10-android-app.md state rules).