Added official hermes-gateway status messages (new comment category instead of tool calls and messages)

This commit is contained in:
ARIA committed 2026-08-20 18:11:36 +02:00
1 parent e9e1aed0f2
commit 86a4c8ee70
7 files changed
+327 -22

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
# Hand-off: Gateway restart UX (spinner + "back online" message)
> This document is a hand-off. A previous attempt at this task is **incomplete
> and leaves the chat in a broken state**. Read this fully before touching code.
## 1. Original task (from the user)
On **gateway restart** the Android/desktop app had two problems:
1. A **spinner spins endlessly** (a tool card that never completes).
2. There is **no "gateway back online" message**.
The user wants the standard hermes routine (same wording on all apps):
```
⚠️ Gateway restarting — Your current task will be interrupted. Send any message after restart and I'll try to resume where you left off.
```
```
♻️ Gateway online — Hermes is back and ready.
```
## 2. Architecture (critical context)
Three layers, only two are editable:
- **`hermes-agent/`** — READ-ONLY research reference (git-ignored, pre-commit
hook blocks commits). The hermes **core** lives here. Do NOT modify it.
- **`gateway-plugin/`** — the editable Python `android` platform plugin
(symlinked into `~/.hermes/plugins/android`). `adapter.py` is the outbound
frame classifier. This is where the gateway turns core `adapter.send()` calls
into WS frames.
- **`app/`** — Compose Multiplatform (Kotlin). `:shared` holds most code
(`jvmMain` is shared by android + desktop). `IrisController` routes frames
into `ChatStore`; `ChatScreen` renders.
### How the hermes core signals a restart (verified in logs + code)
- On **shutdown/restart** the core calls `adapter.send(home_channel, "⚠️ Gateway
restarting|shutting down — …")`. The android adapter classifies this into a
frame. The core **does** send this to the android home channel (log line:
`Sent shutdown notification to home channel android:android:default`).
- On **startup** the core sends "♻️ Gateway online — …" to home channels of
*configured* platforms, but **does NOT send it to the android platform**
(no such log line ever appears). So the app must generate the "online" notice
itself.
### The gateway is a transient systemd user service
- Unit: `iris-hermes-gw.service` (transient, `/run/user/1000/systemd/transient/`).
- **It does NOT auto-revive on SIGTERM.** After a SIGTERM it goes to `failed`
and stays down until you run:
`systemctl --user restart iris-hermes-gw.service`
- Logs: `~/.hermes/logs/gateway.log`. Outbox DB: `~/.hermes/android/outbox.db`
(table `outbox(cursor, chat_id, frame, created)`).
## 3. What the previous attempt changed (all uncommitted, in working tree)
Run `git diff` to see the full patch. Summary:
### App (Kotlin)
- `app/shared/src/commonMain/kotlin/iris/util/TimeFormat.kt` +
`app/shared/src/jvmMain/kotlin/iris/util/TimeFormatJvm.kt`
— added `nowMillis(): Long` (expect/actual) for wall-clock timestamps.
- `app/shared/src/commonMain/kotlin/iris/data/ChatStore.kt`
- `MessageItem` gained `isSystem: Boolean = false`.
- `addSystemMessage(lane, text)` — appends a centered local notice.
- `finalizeInterrupted()` — marks open `ToolItem`s `done=true, ok=false` and
streaming `MessageItem`s `streaming=false`, across all lanes.
- `app/shared/src/commonMain/kotlin/iris/state/IrisController.kt`
- In the `client.state.collect` block, tracks `prevState`:
- `Reconnecting → Connected`: posts `GATEWAY_ONLINE_MSG`
("♻️ Gateway online — Hermes is back and ready.") to the current lane.
- `Connected → Reconnecting`: calls `chat.finalizeInterrupted()`.
- New constant `GATEWAY_ONLINE_MSG`.
- `app/shared/src/commonMain/kotlin/iris/ui/screens/ChatScreen.kt`
- Renders `isSystem` messages via a new centered `SystemMessage` composable
(non-selectable pill).
### Gateway plugin (Python)
- `gateway-plugin/adapter.py`
- New `_is_gateway_lifecycle_notice(content)` — true when content contains
"Gateway restarting" / "Gateway shutting down" / "Gateway online".
- In `send()`, the tool-progress branch now skips lifecycle notices so they
fall through to **commentary** instead of a `tool.start` card.
## 4. Current (BROKEN) state — what the user is seeing
After several test restarts, the chat shows a **stack of repeated entries**:
multiple "⚠️ Gateway shutting down" cards (some with a **spinning** indicator,
some with a red ✗) interleaved with "♻️ Gateway online" pills. The **last**
entry is "Gateway shutting down" even though the app is **connected**. The user
is (correctly) unhappy: the ordering is wrong and a spinner is present.
### The two concrete defects
1. **Wrong ordering / stale "shutting down" last.** On reconnect the app posts
"♻️ Gateway online", but then the **`sync` catch-up replay** re-delivers the
"⚠️ Gateway shutting down" frame that the *old* gateway parked in the outbox
during shutdown. That replayed frame lands **after** the "online" notice, so
the newest line reads "shutting down" while connected.
2. **Spinner returns.** The outbox still contains `tool.start` frames for the
"Gateway shutting down" notice that were written **before** the adapter fix
was loaded. The `sync` replay turns those into tool cards that never receive
a `tool.end`, so they spin. The adapter fix only stops *new* `tool.start`
frames; it does not clean up the already-parked ones.
### Root cause (the part the previous attempt did NOT solve)
The app's **reconnect catch-up (`sync`)** replays the outbox from the stored
cursor, and the app does **not** deduplicate or order these replayed lifecycle
frames against what it already rendered live. Consequences:
- Replayed "shutting down" frames appear after the locally-generated "online"
notice (wrong order).
- Replayed `tool.start` frames (pre-fix) become endless-spinning cards.
- Every restart adds another pair of entries (the "chaos" stack).
The previous attempt treated the symptoms (finalize on disconnect, add an online
notice, reclassify new notices) but did **not** address the replay/dedup/ordering
problem, which is what actually produces the visible mess.
## 5. What is verified vs. not
- **Verified working:** Kotlin compiles (android + desktop), 53/53 Python
`tests/gateway/test_android.py` pass, the "♻️ Gateway online" pill renders,
and *new* lifecycle notices now render as commentary (not a tool card).
- **NOT working / still broken:** the chat accumulates a mis-ordered stack of
"shutting down"/"online" entries and a spinning tool card after restarts.
## 6. Suggested direction for the next engineer
Pick the cleanest of these (or a combination); the goal is: after a restart the
chat shows at most one "⚠️ Gateway restarting" then one "♻️ Gateway online", in
that order, with **no** spinner and **no** accumulation across restarts.
1. **Dedup/order lifecycle notices in the app.** Give the restart/online notices
stable identities and reconcile them (like `MessageItem` is by `messageId`):
a replayed "shutting down" that is already present should not be re-added,
and the "online" notice should supersede the "shutting down" one (replace,
not append). Consider a single "gateway state" line that updates in place
(restarting → online) instead of appending a new line each transition.
2. **Stop the spinner at the source.** Ensure no `tool.start` is ever emitted
for a lifecycle notice (adapter fix is done for new frames) AND make the app
resilient: a `tool.start` with no matching `tool.end` should not spin
indefinitely (e.g., finalize on the next turn, on reconnect, or time out).
3. **Cursor hygiene.** Investigate why the `sync` replay re-delivers frames the
app already rendered live (live broadcasts may not advance the stored
`syncCursor`). If live frames should advance the cursor, the replay would not
re-add them. Check `GatewayClient.dial` (sends `sync`), the `TYPE_SYNC_DONE`
handler in `IrisController`, and the gateway `on_sync`/outbox cursor logic.
4. **Clean the existing mess.** The outbox (`~/.hermes/android/outbox.db`) and
the app's in-memory lanes currently hold the stacked test entries. Decide
whether to prune the outbox and/or reset the app's chat state
(`adb shell pm clear dev.iris.app`) so the user starts clean.
## 7. Commands
- Build/verify app: `cd app && ./gradlew :shared:compileKotlinDesktop :androidApp:compileDebugKotlin`
- Install on phone: `cd app && ./gradlew :androidApp:installDebug` (device `a5ca2a4b`)
- Python tests: `cd hermes-agent && scripts/run_tests.sh tests/gateway/test_android.py`
- Restart gateway: `systemctl --user restart iris-hermes-gw.service`
- Screenshot: `adb exec-out screencap -p > /tmp/shot.png`
- Launch app: `adb shell am start -n dev.iris.app/.MainActivity`
- Reset app state: `adb shell pm clear dev.iris.app`
- Gateway log: `~/.hermes/logs/gateway.log`
## 8. Environment state at hand-off time
- Gateway: **running** (systemd `iris-hermes-gw.service` active).
- App: **installed** on the phone with the (broken) changes above.
- Working tree: 6 modified files (see `git diff`), **nothing committed**.
- The phone's chat currently shows the stacked test entries described in §4.