- upload-artifact@v4+ fails on Gitea/act_runner (GHESNotSupportedError —
the GitHub artifacts API is not implemented), so merge the android,
desktop and release jobs into one: build APK+AAB + desktop zip/deb,
then create the release and upload attachments from the workspace
- jpackage --type deb needs fakeroot, which the runner image lacks —
install it via apt
- sync CI-SETUP.md §3 with the restructured workflow
- pytest lives in hermes-agent's dev extra; plain uv sync left the CI venv
without it and run_tests.sh refused to run (gateway job)
- 'yes | sdkmanager --licenses' dies with SIGPIPE (exit 141) under Gitea's
bash -e -o pipefail; feed a finite number of y's from a file instead
(kotlin + android jobs)
- release.yml android job: build APK + AAB (bundleRelease/bundleDebug)
- androidApp: versionCode overridable via -PappVersionCode (Play requires
an incrementing versionCode per upload)
- make_release_keystore.sh: PKCS12 has no separate key password (keytool
ignores -keypass) — print the store password for ANDROID_KEY_PASSWORD
- Cache.sq / ChatDb: lanes, channels and last_lane persisted as JSON
snapshots; sanitize on restore (Pending->Failed, streaming->false);
ephemeral items (tool, system) skipped
- Platform drivers via expect/actual: AndroidSqliteDriver (app db dir)
/ JdbcSqliteDriver (~/.iris/iris_cache.db)
- IrisController: restore before connect, debounced (750ms) snapshot
persistence, synchronous flush on dispose, clearAll on forget
- History robustness: historyLoaded marked only when the response is
processed (lost request/response retried on reconnect); events
collector wrapped in try/catch; onHelloAck fast path so the history
request fires on the WS thread instead of the starved state
collector; frame-decode and history-load logging
- Protocol: HistoryMessage.media nullable (defensive vs older
gateways that sent "media": null)
- Tests: ChatDbTest (jvmTest, JDBC in-memory), ChatStoreCacheTest,
HistoryPayloadTest, HistoryWireTest (real captured 92KB response)
- docs/10: §10.7 implemented schema, new §10.9 cache behavior
history() wrote "media": null for streaming finals, but the frame
schema says array. The app's HistoryMessage.media was non-nullable, so
deserialization threw and every history page was silently dropped.
Omit the key when media is absent instead.
Deleting a thread, channel, or message was a no-op/soft-delete: messages
were only dropped from the plugin outbox (still in hermes' session store,
hence searchable/recoverable) and channels/threads were merely archived.
Now deletion is complete and non-recoverable, with no search trace:
- purge.py (new): hard-delete from hermes' session store (state.db).
delete_lane wipes a channel's/thread's sessions + messages; deleting a
messages row also drops it from the FTS5 index via the delete triggers.
delete_message removes one message, matched by (session, role, exact
content, closest timestamp) since plugin m_<hex> ids aren't persisted.
- channels.py: delete() hard-deletes the row (and a channel's child
threads) instead of archiving.
- outbox.py: add delete_lane() (wipe all frames for a lane) and
message_info() (read a message's final role/text/ts for the match).
- adapter.py: on_channel_delete wipes outbox + session store;
on_message_delete purges the session-store row per message.
- App: delete confirmations no longer claim history stays for search;
ChannelStore removes a channel's threads on channel delete.
- Docs updated to describe hard deletion.
Hermes can append a text "runtime footer" (model, context %, workdir,
latency, cost) to final replies, but only when display.runtime_footer is
enabled in the hermes config. We want the same info but controlled by the
APP, not the gateway config. So the gateway now ALWAYS sends the data as a
structured `runtime` object on final assistant messages, and the app decides
whether/what to show.
Gateway (gateway-plugin/):
- protocol.py: new runtime_footer() helper + `runtime` field on the
message / message.stop frames. Keys (all optional, absent when the data
is unavailable — e.g. no cost for local models): model (vendor prefix
dropped), context_pct (0-100), cwd (home-relative), latency (seconds),
cost (USD).
- adapter.py: a post_api_request plugin hook captures the turn's model +
prompt tokens + start time (platform-filtered to android so other
platforms don't pollute the buffer). _build_runtime_footer() resolves the
model's context window (cached, best-effort, off the event loop via
asyncio.to_thread with a timeout) and computes context_pct. The runtime
object is attached on every final send (streaming message.stop and
non-streaming message, plus the fallback paths).
- outbox.py: `runtime` preserved in history reconstruction so the footer
survives a restart / first open.
App (app/shared/):
- Protocol.kt: RuntimeMeta data class + `runtime` on MessagePayload /
MessageStopPayload / HistoryMessage.
- ChatStore.kt: `runtime` on MessageItem, wired through live + history
reconciliation.
- SecureStore.kt (+ Android/Desktop actuals): runtimeFooterEnabled +
runtimeFooterFields (persisted per device).
- IrisController.kt: StateFlows + toggleRuntimeFooter() /
toggleRuntimeField(); RUNTIME_FIELD_KEYS / default set / parser.
- SettingsScreen.kt: "Runtime footer" switch; when on, an expandable chip
menu (Model · Context % · Workdir · Latency · Cost) to pick fields.
- ChatScreen.kt: footer rendered on the SAME line as the timestamp (footer
left, time right, Telegram-style), only for final non-streaming assistant
answers; Inspector pane now shows the runtime fields too.
Docs: 04-wire-protocol.md + frames.schema.json document the `runtime`
object.
Verified end-to-end on device: final replies carry
`qwen3.8-27B-exl3-4.5bpw · 53% · ~ · 38s` with the time right-aligned on
the same line; 69/69 gateway tests pass, Kotlin builds + tests pass.
- UserTheme model (ARGB ints) + LocalUserTheme, persisted via SecureStore
- HSV color picker dialog (Apply keeps open, OK applies+closes, Cancel)
- Background: solid color or image (SAF on Android, JFileChooser on desktop)
- Reasoning block: 0.6 black overlay on agent bubble color (adapts automatically)
- Contrast-aware text on bubbles + pinned contentColor on root Surface
- 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.
- Composer input starts at one line and auto-grows up to 5 lines (then scrolls),
measured via onTextLayout; attach (paperclip) moved inside the field, right of
the text, per the Telegram reference.
- New SettingsScreen (drawer/rail entry) for Threads + Tool detail; both now
persist via SecureStore (Android prefs / desktop JSON).
- Add AGENTS.md; rename reference screenshot to telegram_screenshot_reference.jpg.