- gateway-plugin/: android platform plugin (plugin.yaml + adapter.py register(ctx) + no-op AndroidAdapter) + stub modules for M1-M5 - app/: Compose Multiplatform project (shared KMP + androidApp + desktopApp) with Gradle wrapper; builds :androidApp:assembleDebug and :desktopApp:compileKotlin - scripts/guard_hermes_agent.sh + pre-commit hook: fail if hermes-agent/ is staged (read-only reference, never committed) - .gitignore excludes hermes-agent/; docs/ reference library
5.5 KiB
5.5 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 achat_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(defaultandroid:default),kind=default,is_default=true, name "Default". - It is also the cron home channel (
cron_deliver_env_var= ANDROID_HOME_CHANNEL), sodeliver=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(viachannel.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 OFF — flat conversation; all messages use
- Threads are app-organized but gateway-real: each
thread_idis a distinct hermes session lane, so context is isolated per thread and cron can target a specific thread. - The gateway's
create_handoff_threadis used where hermes wants to open a named thread (e.g. continuable cron).
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 mintschat_id = android:chan_<n>, stores in directory, broadcastschannel.createdto all devices. The new channel appears in the channel list.channel.rename/channel.set_default/channel.deletemanage the directory (rename broadcastschannel.renamed; delete is soft — marks archived, keeps history for search).- Cron targeting (the key payoff): because the plugin registers
parse_target_ref_fnandcron_deliver_env_var, cron jobs and thesend_messagetool 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
cronjobtool 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
/croncreation 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). Forplatform:chat_id[:thread_id]it callstools.send_message_tool.resolve_send_target, which uses ourparse_target_ref_fnto parseandroid:<chat>[:<thread>]. - Delivery then calls the live adapter's
send(chat_id, text, …)(gateway running) → our WSmessageframe (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 therole:"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.
6.6 Search
- 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'ssearch.pyopens the session DB read-only and runs FTS5 queries. searchframe →search.results:scope:"all"— search everywhere (all channels/threads/sessions).scope:"chat"— restrict to the givenchat_id(and optionalthread_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).