Runtime footer: app-controlled model/context/cwd/latency/cost under replies

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.
This commit is contained in:
ARIA committed 2026-08-21 19:32:05 +02:00
1 parent 678c0344c8
commit 9286937e2d
13 files changed
+979 -290

No files matched your search

+7 -7
View File
@@ -216,6 +216,7 @@ class Outbox:
"reasoning": payload.get("reasoning"),
"model": payload.get("model"),
"tokens": payload.get("tokens"),
"runtime": payload.get("runtime"),
"ts": payload.get("ts"),
"media": payload.get("media"),
}
@@ -230,6 +231,7 @@ class Outbox:
"reasoning": payload.get("reasoning"),
"model": payload.get("model"),
"tokens": payload.get("tokens"),
"runtime": payload.get("runtime"),
"ts": payload.get("ts"),
"media": None,
}
@@ -259,7 +261,7 @@ class Outbox:
# Omit absent optional fields (the app's serializer treats a
# missing key as its default, but a JSON ``null`` for a
# non-nullable field like ``media`` would fail to parse).
for key in ("reasoning", "model", "tokens", "ts", "media"):
for key in ("reasoning", "model", "tokens", "runtime", "ts", "media"):
if m.get(key) is None:
m.pop(key, None)
return {
@@ -310,12 +312,10 @@ class Outbox:
cursors.append(int(r["cursor"]))
if not cursors:
return 0
placeholders = ",".join("?" * len(cursors))
sql = f"DELETE FROM outbox WHERE cursor IN ({placeholders})"
# Safe: ``placeholders`` is only ``?`` markers; every cursor value is
# bound as a parameter (no user data in the SQL text).
# pi-lens-ignore: python-sql-injection
self._conn.execute(sql, cursors)
# One bound-parameter delete per cursor (a message spans only a few
# frames); same transaction, no string-built SQL.
for cursor in cursors:
self._conn.execute("DELETE FROM outbox WHERE cursor = ?", (cursor,))
self._conn.commit()
return len(cursors)