Files
iris_x_hermes/docs/06-channels-cron-search.md
T
ARIA efecf2732e Auto-threading, history pagination, streaming toggle + tool/reasoning display settings
- Auto-threading (Telegram topic-mode workflow): message.send {auto_thread}
  mints a fresh AI-named thread (instant derived title, LLM upgrade via
  channel.renamed); channel.created {auto:true}; the app jumps into the new
  thread and relocates the optimistic pending bubble.
- history frame: paged full message history for initial channel open /
  scroll-up pagination (reconstructed from the outbox log).
- Streaming on/off: gateway side (display.platforms.android.streaming) plus a
  per-device app toggle (Settings → Streaming); reasoning/model/tokens carried
  on message frames.
- Context menu: long-press (touch) / right-click (desktop) thread affordances
  via a KMP rightClick expect/actual.
- Settings → Reasoning: auto-collapse long reasoning blocks (default on).
- Tool detail: the gateway now always supplies full tool data — it forces
  verbose tool progress (full args → tool.start.args) and captures each
  completed call via the post_tool_call hook (output/duration/ok → tool.end).
  The app reveals the full call + output on expand (Truncated) and
  auto-expands cards in Everything mode.
2026-08-20 16:26:52 +02:00

7.4 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 android:default
A thread (inside default chat) thread_id under the default chat_id chat_id=android:default, thread_id=t_12
A user-created channel a new chat_id android:chan_7
A thread inside a channel thread_id under that chat_id chat_id=android: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 android: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:android: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).

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 = android: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 soft — marks archived, keeps history for search).
  • 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="android" → home (default) channel.
    • deliver="android:android:chan_7" → that channel.
    • deliver="android:android: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 android:<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).