Gateway (docs/19):
- Remove ws_server.py; frame dispatch factored into dispatch.py
- http_server: media upload/pull, pairing over HTTP
- protocol: media frames mirrored; tests + ws_probe updated for HTTP
App:
- HttpGateway: postFrame/uploadMedia/pullMedia no longer throw on
network failure (PostResult ok=false / Result.failure) — uncaught
SocketTimeoutException on Dispatchers.Default crashed the app
- GatewayClient: dead-stream watchdog (health probe every 10s, 2
failures -> redial in ~20s instead of the 45s SSE read timeout);
state flips to Reconnecting when the stream dies, restored from the
last hello.ack on long-poll success; poke() + backoff reset on app
resume (MainActivity.onResume)
- Offline sends: composer enabled while disconnected; a send with no
response (status 0) stays queued (Pending) and is auto-resent on the
next (re)connect after a 2s outbox-replay grace; gateway 4xx
rejections fail the bubble (tap to retry, no auto-loop)
- ChatStore: echo-replace and thread-relocate also match Failed
bubbles (POST response lost in a network drop); loadHistory dedupes
local failed bubbles the server already has; failMessage()
- MainActivity: poke() on resume so a backgrounded app reconnects
promptly instead of waiting out the backoff
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.
- 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.