Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
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.
Hermes does not stop a run when the /v1/runs/{id}/events subscriber goes
away (the SSE handler only detaches it), so runs already survived the
page. But nothing listened any more: an approval request then waited for
nobody and was denied by Hermes's timeout. hermes-voice now follows every
run it starts in its own thread, page or no page; the page replays the
run's items and gets blank keepalive lines so a vanished page is noticed.
On hang-up (button, idle, page close) the page posts /hangup with
keepalive: runs of the conversation count as unheard from then on, and
what the user said since the last delegation becomes a run of its own
with a note that nobody listens. Lines that the model's delegation
already took are not sent again, and a delegation still in its settle
delay leaves them to the hang-up. Only a correction within the same call
(replace) stops the previous run; a new call no longer stops work an
earlier call started.
An approval no call can answer is posted to the owner's Matrix DM (the
gateway's Matrix home room) through the gateway's webhook platform
(--matrix-webhook, signed with the API server key) with a short code;
/approve takes that code, from the page or from the Matrix side with the
API server key.
Approvals also failed outright before: under the gateway's session
authority the approval request is named prompt_id and the answer must be
exactly {request_id, execution_generation, choice}, while hermes-voice
sent only {choice} (invalid_params).
The persona and the server's texts name no particular user: they speak
of "the user" and of the owner's Matrix DM.
voice.lassul.us goes away without a redirect, like other retired names
in the zone. There is no gatus check for it (coaxmetal is not alwaysOn).
With the lazy /nix/store in the machine jobs, hestia's drain read every
cache.lassul.us-only input path to mirror it into the GitHub Actions
cache, downloading 375-760 MiB per job through the lazy store and
spending 1-4.5 min uploading to the Actions cache at 2-11 MiB/s (PR 65
run). Those paths are already in cache.lassul.us; niks3 uploads
everything newly built.
upstream-cache-key-names replaces hestia's default list, so it repeats
cache.nixos.org-1 next to cache.lassul.us-1. In the eval job,
filter-drv-closures applies the same keys to the drv closures; members
signed by cache.lassul.us are substituted from there by the later jobs.
The day side now shows what the geostationary satellites see, like
zoom.earth, instead of the base map under white clouds synthesised from
infrared:
- Europe/Africa/Atlantic: MTG (Meteosat-12) rgb_truecolour from EUMETSAT.
rgb_geocolour looks the same by day but renders ~4x slower on the
server and timed out (502) for full 2048 px tiles.
- Americas/Pacific: GOES-East and GOES-West GeoColor from GIBS.
- Asia/Australia: GIBS has no Himawari colour product. Himawari band 3
(red, 10 min) is turned into white cloud over the base map, with the
cloud opacity being the reflectance beyond the ground's own albedo.
The daily VIIRS true colour mosaics were the alternative, but their
clouds are up to a day old, have swath seams of different times and
would not match the IR clouds next to them.
Each source is weighted by how well its satellite sees the point: full
trust up to 62° view zenith, none from 78° (the limb is smeared), and
the more nadir satellite wins in overlaps with cos^8 of the zenith
angle, so seams are soft. Trust also needs the sun not to be in front
of the satellite: looking towards a low sun both MTG (mornings over the
Indian Ocean) and Himawari (evenings) are washed out to a pale band
with straight edges, while a long path with the sun behind the
satellite is fine (Berlin at winter noon). A plain path-length limit
would have dropped the latter. Near the terminator (cosz < 0.2)
GeoColor fades into its purple night blend, so the live imagery fades
out there and the IR clouds over the base map take over as before.
Where it is shown, the synthetic clouds and so their relief shading and
shadows are scaled down, so the real image is not shaded twice.
All live layers (including the IR clouds) now request an explicit TIME,
the render time floored to 10 min: without TIME GIBS served tiles of
one layer from different frames (sun glint cut at a tile edge), and it
makes --time render the imagery of that time, not only the sun. The
TIME is capped at the newest frame the server lists (GIBS
DescribeDomains, EUMETSAT per-layer capabilities), since GIBS answers
an unpublished time with an empty image and EUMETSAT with an error;
MTG true colour stops producing frames while its disk is dark.
GIBS also now and then answers with an empty 1 kB image for a frame it
has (the same request succeeds when repeated), which left a quarter of
the Himawari disk missing, so empty tiles are asked for up to 3 times.
A machine job substituted the whole input closure of the derivations it
builds, e.g. 3.4 GiB for neoprism, of which the builds open a few dozen
MiB. .github/lazy-store.sh now runs before the build: it takes what
`nix build --dry-run` would fetch, collects its closure from the
narinfos of cache.nixos.org and cache.lassul.us (tools.lazy-store),
mounts lazy-store on /nix/store, registers the paths with
`nix-store --load-db` and copies their signatures. The build itself,
ci-build.sh's niks3 push and hestia stay as they were: outputs are
written to the real store dir below the mount, and ci-build.sh's
before/after diff of the store starts after the registration.
If anything fails before the paths are registered, it logs an error,
undoes the mount and exits 0, so the build substitutes eagerly. The FUSE
log and the fetched NARs are printed by an always-run step after the
build.
The eval job builds lazy-store next to niks3 and pushes it, the machine
jobs substitute it. The eval and shared jobs keep the plain store: eval
builds nothing but niks3 and lazy-store, and shared builds package
derivations, which read most of their inputs anyway.
FUSE file system for CI's machine builds, ported from the experiment on
ci-lazy-fuse (.github/exp/lazy-store). It mounts on /nix/store the real
store dir (all writes go there) merged with store paths of
cache.nixos.org and cache.lassul.us, whose metadata comes from their
.ls listings and whose NAR is fetched, checked and unpacked on the first
open() of a file in them; reads go through kernel FUSE passthrough. In
the experiment, rebuilding neoprism's system-units, etc, system-path and
toplevel downloaded 62 MiB instead of the 3,455 MiB of an eager
substitution.
Changes from the experiment:
- closure takes the store paths to make valid on stdin (what
`nix build --dry-run` would fetch) instead of walking derivations, and
leaves out paths whose closure is not entirely in the caches, so that
nix never sees a valid path with a missing reference; nix substitutes
those as usual.
- closure also writes the fetched narinfos as a file:// binary cache:
`nix-store --load-db` registers no signatures, and niks3's
--ignore-upstream-key and hestia's upstream-cache-filter go by the
signatures in the local database, so without them both would upload
(and thereby download) every cache.nixos.org path in an output's
closure. `nix store copy-sigs` adds them from there.
- mount fails if a path has no listing instead of leaving it out, as
every path gets registered.
Static, so that once it serves /nix/store it loads nothing from there.
A flake package, so CI builds it once and pushes it to cache.lassul.us,
and the jobs substitute it. passthru.tests.fetch covers fetch, closure
and the registration with signatures against a file:// cache.
The GIBS VIIRS_NOAA20_Thermal_Anomalies_375m_All raster shows each
detection as a coarse blob, far bigger than its 375 m footprint at
wallpaper scale, so the fire layer was loud and distracting.
Fetch FIRMS' keyless global 24 h VIIRS CSVs of NOAA-20, NOAA-21 and
Suomi NPP instead (cached, refreshed hourly like the old layer), drop
low-confidence detections and draw each one as an antialiased dot in
screen space through the same forward mapping as the markers: about
1 px at 1080p, growing slightly with the lens zoom, opacity from FRP on
a log scale between 1 and 500 MW. The colour and max blend under the
clouds stay as before.