feat(#2050): set matrix avatar in the daemon over the live client

This commit is contained in:
damocles 2026-06-27 19:35:21 +02:00 committed by mara
commit c99fa714d9
8 changed files with 118 additions and 210 deletions

View file

@ -396,39 +396,35 @@ agent containers come up. Without the path-trigger sibling
first time it ran and the MCP would have no backend until the next
restart. The `.path` unit makes the appearance of the token re-fire
the service so the daemon comes alive in the same boot cycle as
provisioning. `matrix-avatar-sync.path` uses the same pattern for
the icon-upload oneshot.
provisioning. The same token watcher also drives avatar setting: on a
restart the daemon re-runs each account's bring-up, which sets the
avatar (see below).
### matrix-avatar-sync (two-step media + profile dance)
### matrix avatar (set by the daemon over the live Client)
Mirrors the forge-avatar oneshot's shape (`docs/conventions.md::
Best-effort oneshot services`) but differs in protocol: matrix
avatars are a two-step `POST /media/r0/upload` → `PUT
/profile/<user_id>/avatar_url` dance, both authenticated by the
`access_token` written by `hive-c0re::matrix::ensure_user_for` to
`<state>/matrix-token`.
The agent icon (`hyperhive.icon`, an SVG) is published as each matrix
account's profile avatar by `hive-matrix-daemon` itself
(`hive-matrix-mcp::client::sync_avatar`), not a separate oneshot. After
the daemon builds + restores an account's `Client` (authenticated,
pointed at that account's resolved homeserver), it calls matrix-sdk's
`account().upload_avatar()` — one call that uploads the media and sets
`avatar_url`. Because it reuses the live Client, there is no hardcoded
homeserver URL, no token re-read, and no token-file globbing: the daemon
already iterates every configured + dashboard-discovered account in its
bring-up loop, so the avatar is set for **every** account.
The oneshot syncs the avatar for **every** account, not just the
hive-internal one: a `sync_one` shell function runs the upload + set
dance for the bare `matrix-token` (`main`, against `localhost:8008`)
and then for each dashboard-provisioned `matrix-token-<name>`, reading
that account's homeserver from its `matrix-account-<name>.json` sidecar
(mirrors `accounts::discover_token_accounts`). Idempotency is
**per-account**: a `matrix-avatar-icon-hash[-<name>]` file each, because
the `mxc://` URI is homeserver-scoped so every account uploads to its own
homeserver independently.
Nix rasterizes the SVG to a 512x512 PNG at build time (`iconPng`, via
librsvg) and forwards its store path as `HIVE_ICON_PNG` on the daemon
unit, gated on `hyperhive.icon != null`. No icon configured → the env is
unset → `sync_avatar` returns early and no avatar is set.
Triggered by EITHER boot (`wantedBy = multi-user.target`) OR the
sibling `matrix-avatar-sync.path` firing on token appearance — its glob
covers both `matrix-token` and `matrix-token-*`, so an account logged in
after boot gets its avatar without a restart. Both paths re-run the
oneshot idempotently — running the avatar set twice is harmless.
Critically: **`RemainAfterExit = false`** (not the more common
`true` for oneshots). systemd treats `RemainAfterExit = true`
oneshots as "still running" after the first exit, so the second
trigger from the `.path` watcher becomes a no-op. Setting it to
`false` lets re-fires actually re-execute. The trade-off is the
service unit shows `inactive (dead)` between fires — visible in
`journalctl` but harmless; the `.path` unit drives the lifecycle.
Idempotency is **per-account**: an `avatar-icon-hash` file in each
account's matrix-sdk `state_dir`. The daemon hashes the PNG bytes and
skips the upload when unchanged, because every upload mints a fresh
`mxc://` URI that emits a profile state event in every joined room —
re-uploading identical bytes is timeline spam. A dashboard-provisioned
account gets its avatar when the `systemd.paths.hive-matrix-daemon` token
watcher restarts the daemon (which re-runs the per-account bring-up), so
no separate avatar trigger is needed. Avatar failures are swallowed
(logged, non-fatal) so they never break account bring-up or sync.