Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
audio-matrix (./matrix.py) watches PipeWire (pw-dump --monitor) and
publishes per source and room an MQTT discovery switch
switch.audio_route_<source>_<room> (attributes source, speaker, kind),
grouped as one HA device per source. Sources are AirPlay (shairport's
"Shairport Sync" stream), LAN (roc-lan) and every connected Bluetooth
phone (bluez_input.*, keyed by MAC, named by its alias); a phone's switches
are removed when it disconnects. The daemon links the source node directly
to roc-<room> with pw-link and re-applies the links whenever nodes come
back (phone reconnects, PipeWire restarts).
The routes are kept per source key in /var/lib/audio-matrix/routes.json, so
a reconnecting phone gets its previous routing. A source never seen before
plays on all rooms: a freshly paired phone should just make sound.
The hub null sink, the "Audio room" select, its buttons, the audio-room
service and command are gone. WirePlumber must not link the sources itself
any more: node.autoconnect = false for bluez_input.* (monitor.bluez.rules
run after name-node.lua sets it true) and roc-lan (source.props), and
PIPEWIRE_AUTOCONNECT=false for shairport-sync, whose pipewire backend
always connects with PW_STREAM_FLAG_AUTOCONNECT. Groups without the null
sink are driven by the phone's node or PipeWire's Dummy-Driver.
The old discovery configs stay retained on mosquitto; clear them once on
acheron:
for t in homeassistant/select/audio_room/config \
homeassistant/button/audio_room_{shodan,icarus,both,off}/config \
gg23/audio-room/set gg23/audio-room/state; do
mosquitto_pub -h localhost -u gg23 -P gg23-mqtt -r -n -t "$t"
done
A Pixel 9a with HD audio negotiated a high-quality codec, and WirePlumber
failed to create the bluez_input node ("mod.adapter: can't get format:
Invalid argument"), so no audio reached the hub and the phone retried
every second. With SBC it works. AAC decoding is available: the
nixpkgs pipewire bluez5 aac plugin links fdk-aac. sbc_xq shares the
sbc endpoint and has a decoder.
llm-agents now ships orca 1.4.227, whose release exists again, so the
1.4.224 pin (for the deleted 1.4.225/1.4.226 releases) and its assert
are no longer needed.
yellow mounts the same ZFS dataset (tank/download) as neoprism's
/var/download, which is covered by DownloadSpace*, so DiskAlmostFull
fired for it. Match the exemption on the device of neoprism's series
instead of host and mountpoint.
sync-containers3 containers (radio, yellow) get the host's
/nix/var/nix/db bind-mounted read-only, so nixos-upgrade failed every
night with "opening lock file /nix/var/nix/db/big-lock: Read-only file
system". Talk to the bind-mounted daemon socket instead, like the
autoswitch service does. Only for boot.isContainer, so other machines
are unaffected by that part.
The guard reported "cannot reach" for any nix flake metadata failure,
including this one; say what actually happened instead. nix's own
error is already on stderr above it.
Load the hermes-voice plugin into the gateway (matrix_dm_send, end_call,
/assist), and enable the gateway's webhook platform on 127.0.0.1:8644
with a deliver_only route to the Matrix home room, through which
hermes-voice asks approvals that no call can hear. The route is signed
with WEBHOOK_SECRET, which hermes-env fills with the API server key that
hermes-voice already holds: no new secret.
gpt-live-1 with client delegation has no client-side function tools: the
only model-to-page signal is session.delegation.created, which carries no
name or arguments. So the voice model does not hang up itself: goodbye,
"that's all", "hang up" and "I'm leaving" are delegated like any request,
and Hermes decides. BACKEND_NOTE tells it to call end_call once requested
work has been started or handed off, never while it still needs an answer
such as an approval.
end_call lives in the gateway plugin. Plugin handlers get the calling
session_id (model_tools.py passes it to registry.dispatch), and the
session context names the platform. The tool succeeds only on the API
server in a session whose lineage starts at a voice-* id: context
compression continues a session under a new id with the old one as
parent_session_id, so the prefix alone would not survive a long call.
Elsewhere it returns an error and changes nothing.
hermes-voice sees the tool in the run's events (tool.completed of
end_call without error) and passes an end_call item to the page instead
of a tool item. The page hands Hermes's reply to the voice model as
usual and closes the session once the voice has spoken it and been quiet
for 800 ms, at the latest after 30 s; close() now runs only once. A run
no page listens to (hang-up flush, page gone) only logs it.
Hermes registers no agent-callable send_message tool on any platform
(tools/send_message_tool.py: "intentionally NOT registered as an
agent-callable model tool"; toolsets.py: "deliberately no agent-callable
send_message tool"), so there is no config switch, platform toolsets
included, that lets voice sessions on the API server write to Matrix. The
plugin adds matrix_dm_send, limited to the Matrix home room (the owner's
DM) and sent through the gateway's live adapter by Hermes's own send
helper, which also mirrors the message into the DM's session. Reading the
DM needs nothing new: session_search (chat_history_lookup) already reads
the Matrix session's transcript from any session.
/assist approve|deny [code] answers an approval hermes-voice asked in the
DM. A Matrix /approve cannot do it: it only resolves approvals of the
chat's own session, and API runs are keyed by run id. /voice is taken by
Hermes itself.