Commits
Replace the greetd gamescope-session (which required STROM_NO_GAMESCOPE and
rendered games directly in the session compositor) with greetd launching sway.
A game's nested gamescope prefers its wayland backend, which needs wlroots
protocols that cage (and, differently, an outer gamescope session) do not give
it. Under those it fell back to gamescope's SDL backend, whose Vulkan swapchain
remake loops and aborts on this RADV RENOIR iGPU (rendervulkan.cpp
vulkan_remake_swapchain) - crashing e.g. Baba Is You a couple seconds in. sway
exposes those protocols, so gamescope inits its wayland backend and never hits
the crashing SDL path. Every game keeps its own gamescope (render resolution,
in-game exit chord, bare-tty portability); no per-game workaround needed.
Also resolve STROM_FLAKE to the /home/strom/strom checkout when present (github
fallback) so changes can be tested on the box before pushing upstream.
Xbox pads pair via JustWorks; on reconnect a pad whose bond went stale
re-initiates pairing, which needs a registered agent to auto-confirm. bluetuith
only has an agent while actively pairing in it, so an autonomous re-pair hit
"No agent available for request type 2" and the link dropped right after
connecting. Run bt-agent -c NoInputNoOutput as a persistent default agent so
pairing/re-pairing always completes.
Also set TemporaryTimeout=30 so the LAN party's hundreds of stray BLE devices
are evicted from the cache instead of piling up and saturating the adapter.
The bespoke pairing script was fighting Xbox BT quirks (stale bonds,
pairing-mode timing) more than it helped. Remove it; bluetuith (already
installed via 2configs/pipewire.nix) is a cleaner TUI for pairing/managing
controllers.
A pad paired to another device in the meantime still shows Bonded here, so the
old script skipped it and it never reconnected (kept shutting down / rejoining
the other host). Target pads by what actually shows up in the scan (present and
advertising in pairing mode) rather than the bonded-device list, and clear any
stale bond before re-pairing. Pads already connected and working are left
alone.
Replace the cage kiosk with a gamescope session on DRM (greetd autologin).
gamescope presents straight to the display, so there is no nested Wayland
swapchain for RADV to storm on this Vega iGPU - which crashed games like Baba
Is You when gamescope was nested under cage. Set STROM_NO_GAMESCOPE=1 so the
launcher runs <slug>.no-gamescope and each game renders directly in the session
compositor instead of nesting its own gamescope.
Runs the patched launcher from the /home/strom/strom checkout until the
STROM_NO_GAMESCOPE support lands upstream (github launcher lacks it).
Add strom-pair (in-session NoInputNoOutput agent + polling, bond check) to
pair Xbox Wireless Controllers one at a time, plus the bluez/kernel bits they
need: hardware.xpadneo, disable_ertm=1 (else AuthenticationFailed), and
FastConnectable=true so bonded pads reconnect. Point the launcher session at
evdev via SDL_JOYSTICK_HIDAPI_XBOX=0, poll hotplug with
SDL_JOYSTICK_DISABLE_UDEV=1, and allow input without focus via
SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1.
Autostart the strom couch launcher fullscreen via services.cage on boot
(after LUKS unlock), always pulling the newest launcher with nix run.
Dedicated strom kiosk user; force backlight to max on boot and let the
video group adjust brightness via brightnessctl udev rules.
mpvpaper renders a random video/image from ~/wallpaper on the background
layer, rotated daily; noctalia's own wallpaper is disabled to let it show
through. lockscreen picks from the same folder. Seeds a moewalls pixel-art
sunset starter. Drops the unused feh fetchWallpaper on ignavia.
Keep the default wpa_supplicant backend (iwd pegged 100% CPU in dense
~100-AP RF and wedged the link), disable NIC power-save + PCIe ASPM to
cut mt7925 scan-deauth frequency, and drop the gateway-ping watchdog
(bornhack drops ICMP to the gateway, so it false-fired and bounced a
working connection every 90s).
Mark wlan links unmanaged by systemd-networkd so NetworkManager is
the sole wifi DHCP client. With networking.useNetworkd enabled, the
auto-generated 40-<iface>.network ran a second DHCP client on the
wifi; the two fought over the lease and routes and dropped the link
every few minutes. Match by Type=wlan so it holds across interface
renames.
nixvim's docs build patches nixos-render-docs with nixpkgs PR#538502,
already merged in our nixpkgs, so the patch fails and breaks the build.
We follow a single nixpkgs, so drop the manpage instead.
nixpkgs default python3 moved to 3.14, but tulir_telethon declares no
3.14 support, breaking evaluation.
The list_add corruption / lib/list_debug.c:32 freezes on WiFi roam were
an add-side race: generic mt76_sta_add() re-ran mt76_wcid_init() on a
wcid that mt7925_mac_sta_add had already published, letting the rx/txs
softirq link it into sta_poll_list right before the second
INIT_LIST_HEAD self-looped the node. Our local teardown-side patch
could not close that window (crashed again 2026-07-07, 4x in 11 min).
Upstream fix 20b126920a25 ("wifi: mt76: add wcid publish check in
mt76_sta_add") is in 7.1.3, which linuxPackages_latest now provides
after the nixpkgs bump. Keep kernel >= 7.1.3.
Six hard freezes on 2026-07-03, all during WiFi roaming between hoelle
APs. Captured oopses show identical list_add corruption in an 8k slab
(ieee80211_sta + drv_priv) -> kernel BUG at lib/list_debug.c:32 in
softirq context -> full system freeze.
Root cause: mt792x station teardown never clears wcid->sta, so the
rx/txs softirq path can re-add a dying wcid to sta_poll_list after
mt76_wcid_cleanup() unlinked it. Once the sta is freed and the slab
slot reused, mt76_wcid_init() self-loops the still-linked list node;
the next list_add_tail() panics. Also fixes the unlocked
list_first_entry read in mt7925_mac_sta_poll (mt7921 already takes
the lock there).
Full analysis in the patch header. Drop once fixed upstream
(https://github.com/zbowling/mt7925 tracks related work).
Also drop amdgpu.mes=0: the parameter no longer exists on 7.x
("amdgpu: unknown parameter mes ignored").
Replace the stock chime with the quack from
https://www.youtube.com/watch?v=Fw3RB7xnb80, trimmed to 0.2s and
attenuated to match the original's peak level.
Assisted-by: Claude Code (claude-fable-5)
Matrix is a Tier-2 platform whose default tool-progress preview caps
commands at 40 chars, so a `terminal` call only surfaced e.g.
`curl -X POST -H "Content-Type: applic...`. Set the matrix platform's
tool_progress to "verbose" so the gateway renders the full, multi-line
command as a fenced code block.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The Hermes Agent ships a "claude-code" skill that delegates coding via
`terminal(command="claude -p '...'")`, but the `claude` binary wasn't on
the agent's PATH. Add it through services.hermes-agent.extraPackages, which
lands in both the systemd service PATH and the hermes user's profile.
Auth is separate: run `sudo -u hermes -H claude` on the host once.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The flock around `ipfs add --nocopy` in the watcher and reconcile sweep
guarded against a corruption that was assumed, never observed: it was
added defensively alongside the reconcile sweep and never exercised
(the MFS deadlock masked it).
Tested it directly on kubo 0.40.1: 10 trials of two concurrent
`ipfs add --nocopy --pin` on the same 16MB file, no lock. Every trial
produced the same CID, a complete DAG (refs -r), and byte-identical
content (cat | sha256sum). Concurrent same-file --nocopy adds are
idempotent, so the lock guards nothing -- remove it.
kubo 0.40.1 has a daemon-side MFS goroutine deadlock: the first
`ipfs files` write after each daemon start permanently wedges the global
MFS lock, so even a trivial `files mkdir` then hangs forever. This
silently parked the pin-watcher on its startup `files mkdir` and stopped
auto-pinning for ~4 days. It is not fixable client-side (timeout/flock/
serialize cannot release a lock held by a wedged daemon goroutine).
Rip out the browse-index entirely rather than gate it behind a toggle:
- drop the publish oneshot + timer (IPNS publish of the MFS root)
- drop all `ipfs files` ops from the watcher and reconcile, plus the
mfs()/mfs_add/mfs_rm/mark_dirty/relpath helpers, IPNS keygen, startup
mkdir, backfill, and the dirty-flag/mfs-lock machinery
- rename flix-index-reconcile -> ipfs-pin-reconcile (it is pin-only now)
Pinning is unaffected: the inotify watcher still `ipfs add --nocopy
--pin`s under the pin-add flock, and the hourly forward-sweep reconcile
still heals dropped inotify events.
The pin-watcher loop is single-threaded and blocks 1-2 min on each multi-GB
`ipfs add`. While blocked, a concurrent upload's CREATE,ISDIR + MOVED_TO for a
brand-new game subdir can overflow the inotify queue and be dropped, leaving a
fully-written file that is never pinned (gateway 504s on it forever). The
hourly reconcile only did reverse cleanup (drop entries for vanished files), so
a dropped event was permanent until a watcher restart re-ran the startup sync.
Add a forward sweep to the reconcile: pin on-disk files older than 10 min that
are missing from cid-map. Guard both the watcher's add_file and the sweep with
a shared flock (pin-add.lock) so two concurrent --nocopy adds of the same file
can never corrupt the filestore DAG. flock auto-releases on process death/OOM/
reboot, and an add interrupted mid-pin never writes its cid-map line, so the
sweep re-adds and completes it on the next pass.
Bot accounts (Hermes, bridges) burst many events per turn — thinking panes,
reactions, redactions, chunked replies — and hit the default caps, causing
M_LIMIT_EXCEEDED dropped sends and redactions.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Run Nous Research's Hermes Agent as a native systemd gateway on coaxmetal,
reachable as @hermes:lassul.us over Matrix (E2EE). Wires upstream's
nixosModules.default via a new flake input.
Backend is Nous Portal (one key, 266 models) driving an agentic model
(MiniMax M2.5) through the generic OpenAI-compatible provider — hermes's
built-in nous provider is OAuth-only against a dead endpoint, and Nous does
not recommend the Hermes-4 chat models for the agent. Secrets (Matrix access
token, Nous API key) come from clan vars; a startup DNS-wait guards the
IPv6-only homeserver against a boot race.
Assisted-by: Claude Code (claude-opus-4-8[1m])
A room's unread in cinny is the main-timeline notification count plus the
sum of every thread's count (Room.getUnreadNotificationCount sums
threadNotifications), and roomHaveNotification keys off that. The
room-level Mark-as-Read sent a receipt only for the latest non-thread
event, clearing the main timeline but leaving every thread count intact -
so a room with any unread thread stayed bold in the sidebar even after
being marked read, with no per-thread indicator to explain why.
Send an unthreaded read receipt against the room's latest event instead;
the SDK/server treat it as clearing the main timeline and every thread's
unread bucket at once.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Nix store files have mtime=1970 and the vhost set no Cache-Control, so a
browser can heuristically cache index.html for a long time and not pick
up a newly deployed build on reload. Serve index.html with no-cache
(revalidated cheaply via etag) and the content-hashed /assets/ with a
long immutable max-age.
Assisted-by: Claude Code (claude-opus-4-8[1m])
markThreadAsRead sent the receipt against thread.events[last]. Two ways
that fails to clear the thread server-side:
- The SDK's threadIdForReceipt() routes a receipt for the thread root,
or a relation to it (e.g. a reaction on the root), to the "main"
timeline, which never touches the thread's unread bucket.
- Receipting an event that is not the thread's true latest reply leaves
a residual server-side count, so the notification reappears on the
next sync/reload even for threads that were opened.
Target thread.lastReply() - the SDK's canonical latest m.thread reply,
taken from the authoritative thread timeline (including the bundled
latest_event from /threads) and the same event the SDK uses for its
read-up-to comparison. Fall back to a newest-first scan of the live
timeline / rendered replies only when the Thread object is not hydrated.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The thread unread badge in RoomViewHeader only recomputed on
ThreadEvent.New/Update/NewReply. Marking a thread read emits
RoomEvent.Receipt / RoomEvent.UnreadNotifications instead, so the badge
kept a stale count until unrelated thread activity fired a recompute -
the notification appeared persistent and laggy. Subscribe the badge
effect to those receipt events too.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Its fetchPnpmDeps used fetcherVersion=2, removed in nixpkgs 26.11, breaking
`nix flake check`. Drop the package and its unused (un-imported) service module.
Assisted-by: Claude Code (claude-opus-4-8[1m])
nixpkgs 26.11 systemd.nix now reads environment.variables.SYSTEMD_XKB_DIRECTORY
and config.i18n.imperativeLocale, but extra-container's minimal module set
omits the modules that declare them, breaking the container-virtulus eval in
`nix flake check`. Add dummy options to the existing workaround block.
Assisted-by: Claude Code (claude-opus-4-8[1m])
bitwarden-desktop pulls electron_39, which nixpkgs now marks insecure
(EOL), failing `nix flake check` (eval-xerxes). secureify it via overlay
rather than nixpkgs.config.permittedInsecurePackages, matching the
existing olm fix in mautrix-telegram (lib/secureify.nix).
Assisted-by: Claude Code (claude-opus-4-8[1m])
orange's autoswitch was failing activation: the git service's
mkdir /var/lib/git hit a symlink loop (a stale dangling
`state/git -> ../../var/state/git` link that loops because the container
bind-mounts both /var/lib and /var/state to the same state dir).
cgit is being retired, so remove the services/git import entirely. This
also unblocks orange's autoswitch (it no longer touches /var/lib/git).
Assisted-by: Claude Code (claude-opus-4-8[1m])
A transient GitHub 5xx while resolving the flake at boot used to leave a
container's autoswitch in a failed oneshot state with no retry, silently
running stale config (observed on yellow: the disk-space guard never
updated to the 100 GiB threshold).
Add Restart=on-failure with RestartSec=120s. Since RestartSec is well
above the default StartLimitIntervalSec, the restart rate limiter never
trips, so a transient failure keeps retrying every 2 min and self-heals
once GitHub recovers.
Assisted-by: Claude Code (claude-opus-4-8[1m])
ollama is no longer used here; removing it frees ~4G.
Prepare a fresh libvirt guest for riot on a dedicated public IP
(95.217.192.52, separate MAC) bridged directly onto ext-br. Because the
bridge is unmanaged, libvirt installs no iptables rules for it, avoiding
the nat-chain race that forced the old restart-iptables hack in hfos.nix.
- physical.nix: enable the ext-br bridge, enslave enp35s0, host IP on bridge
- config.nix: enable libvirtd, add riot login user, FORWARD accept for the VM IP
Bridge cutover applies at the next reboot window; the VM is created
imperatively afterwards.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Cinny has no thread overview UI, which makes the Zulip bridge's stream rooms
(one Matrix thread per Zulip topic) hard to navigate. Carry smokku's
"threads-ui" thread drawer/browser (PR cinnyapp/cinny#2787, closed only
because Cinny froze PRs during their SDK rewrite) as a patch over the
packaged v4.12.2 release and serve it at the matrix.lassul.us root.
The patch is the squashed feature diff resolved against v4.12.2; the only
conflict was RoomViewHeader.tsx (additive, alongside the call button). The
lockfile is unchanged so nixpkgs' npmDepsHash still applies. Re-resolve the
patch against the new tag on each cinny bump.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Bridge Matrix to Zulip via GearKite/MatrixZulipBridge (packaged in nixpkgs
as matrix-zulip-bridge). No upstream NixOS module exists, so the systemd
service mirrors the heisenbridge module it is forked from: generate the
registration, merge in the tokens, and wire it into Synapse. Zulip orgs
(e.g. zulip.geninf.io) are configured at runtime via the bridge admin room.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The security.dhparams module was removed from nixpkgs (RFC 7919), but
HedgeDoc's HTTPS listener unconditionally readFileSync()s dhParamPath at
startup. With the path unset it read a directory (EISDIR) and crash-looped,
taking pad.lassul.us down. Point dhParamPath back at the existing file and
add an ExecStartPre that regenerates it with openssl if missing.
Assisted-by: Claude Code (claude-opus-4-8[1m])
/var/download churns constantly (download temp files appearing and
vanishing), so `find` exits non-zero when an entry disappears between
readdir and stat. Under `set -efu` that aborted the oneshot, which during
a nixos switch counted as a failed unit and flipped the whole switch to a
non-zero exit. Tolerate find's exit code and only replace the live index
if a non-empty listing was produced.
Assisted-by: Claude Code (claude-opus-4-8)
When /var/download drops below 100 GiB free the guard now stops all
transmission torrents (it has no native low-disk pause like sabnzbd's
download_free) and posts a Matrix message to !MRjkvAaFPsjdYCpeiZ:lassul.us
via the archiver bot's access token. Torrents are restarted and a
recovery message sent once space climbs back above the threshold. State
is tracked in StateDirectory so alerts fire only on transitions, not
every run.
Assisted-by: Claude Code (claude-opus-4-8)
The previous guard only throttled sabnzbd below 100 GiB and paused at a
15 GiB floor, which still let the disk fill close to full. Use sabnzbd's
native download_free/complete_free (continuous pause + auto-resume) set
to 100 GiB so it pauses the moment free space drops below that, and have
the guard keep those configured. Transmission has no equivalent low-disk
guard, so the guard still throttles its downloads to a trickle below the
threshold while leaving seeding alone.
Assisted-by: Claude Code (claude-opus-4-8)
Assisted-by: Claude Code (claude-opus-4-8)
Recent wireguard-tools resolve the peer Endpoint in the namespace where
`wg syncconf` runs. The transmission netns has no route until the tunnel
is up, so resolving europe.vpn.airdns.org from inside it always fails;
wg burns ~70s of DNS-retry backoff per attempt before erroring. During a
nixos switch that restarts transmission-netns this stalled the whole
deploy for >2min and left the service (and the download stack) failed.
Resolve the endpoint in the main netns (which has connectivity, and is
already gated on the `host` check above) and hand wg a numeric Endpoint,
so the tunnel comes up instantly. Also fix the bare `sleep` in the DNS
wait loop, which errored under `set -e` instead of retrying.
Assisted-by: Claude Code (claude-opus-4-8)
Adds download-space-guard: a 5-min oneshot that caps sabnzbd and
transmission speeds once free space on /var/download drops below 100 GiB,
and pauses sabnzbd below a 15 GiB floor. This prevents a full disk from
hard-wedging sabnzbd with an unrecoverable "Disk full! Forcing Pause";
the guard auto-resumes once space frees up.
Assisted-by: Claude Code (claude-opus-4-8)
/boot is only 1 GiB (disk.nix). Without configurationLimit, GRUB retains
every generation's kernel+initrd until the ESP fills, at which point the
bootloader install fails ("No space left on device") and deploys can't
finish. Keep the 10 most recent; older kernels get pruned automatically.
Assisted-by: Claude Code (claude-fable-5)
The whole system lives on one btrfs-on-LUKS-on-NVMe device, so downloads
and builds were starving latency-sensitive reads — launching a terminal
would freeze for seconds (PSI full IO pressure spikes, near-zero CPU/mem
pressure). Three independent fixes:
- mq-deadline scheduler on nvme0n1: the default "none" gives no read/write
fairness, so reads queued behind up to 1023 writes.
- vm.dirty_bytes/dirty_background_bytes as small fixed values instead of
the ratio defaults (~6/12 GB on 62 GB RAM) that caused multi-GB flush
bursts.
- bypassWorkqueues on the LUKS device to drop the kcryptd serialization
point (safe: AES-NI is present).
Assisted-by: Claude Code (claude-fable-5)
The clan bump removes the `clan.core.vars.generators.<name>.migrateFact`
option, which made every host fail to evaluate. Fact migration is a
one-time step that's long complete, so drop the now-removed option from
all generators (retiolum, syncthing, shadowsocks, hedgedoc, mycelium,
hotdog/orange containers, nextcloud, wiregrill, nix-serve, matrix).
Assisted-by: Claude Code (claude-fable-5)
Replace the greetd gamescope-session (which required STROM_NO_GAMESCOPE and
rendered games directly in the session compositor) with greetd launching sway.
A game's nested gamescope prefers its wayland backend, which needs wlroots
protocols that cage (and, differently, an outer gamescope session) do not give
it. Under those it fell back to gamescope's SDL backend, whose Vulkan swapchain
remake loops and aborts on this RADV RENOIR iGPU (rendervulkan.cpp
vulkan_remake_swapchain) - crashing e.g. Baba Is You a couple seconds in. sway
exposes those protocols, so gamescope inits its wayland backend and never hits
the crashing SDL path. Every game keeps its own gamescope (render resolution,
in-game exit chord, bare-tty portability); no per-game workaround needed.
Also resolve STROM_FLAKE to the /home/strom/strom checkout when present (github
fallback) so changes can be tested on the box before pushing upstream.
Xbox pads pair via JustWorks; on reconnect a pad whose bond went stale
re-initiates pairing, which needs a registered agent to auto-confirm. bluetuith
only has an agent while actively pairing in it, so an autonomous re-pair hit
"No agent available for request type 2" and the link dropped right after
connecting. Run bt-agent -c NoInputNoOutput as a persistent default agent so
pairing/re-pairing always completes.
Also set TemporaryTimeout=30 so the LAN party's hundreds of stray BLE devices
are evicted from the cache instead of piling up and saturating the adapter.
A pad paired to another device in the meantime still shows Bonded here, so the
old script skipped it and it never reconnected (kept shutting down / rejoining
the other host). Target pads by what actually shows up in the scan (present and
advertising in pairing mode) rather than the bonded-device list, and clear any
stale bond before re-pairing. Pads already connected and working are left
alone.
Replace the cage kiosk with a gamescope session on DRM (greetd autologin).
gamescope presents straight to the display, so there is no nested Wayland
swapchain for RADV to storm on this Vega iGPU - which crashed games like Baba
Is You when gamescope was nested under cage. Set STROM_NO_GAMESCOPE=1 so the
launcher runs <slug>.no-gamescope and each game renders directly in the session
compositor instead of nesting its own gamescope.
Runs the patched launcher from the /home/strom/strom checkout until the
STROM_NO_GAMESCOPE support lands upstream (github launcher lacks it).
Add strom-pair (in-session NoInputNoOutput agent + polling, bond check) to
pair Xbox Wireless Controllers one at a time, plus the bluez/kernel bits they
need: hardware.xpadneo, disable_ertm=1 (else AuthenticationFailed), and
FastConnectable=true so bonded pads reconnect. Point the launcher session at
evdev via SDL_JOYSTICK_HIDAPI_XBOX=0, poll hotplug with
SDL_JOYSTICK_DISABLE_UDEV=1, and allow input without focus via
SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1.
Keep the default wpa_supplicant backend (iwd pegged 100% CPU in dense
~100-AP RF and wedged the link), disable NIC power-save + PCIe ASPM to
cut mt7925 scan-deauth frequency, and drop the gateway-ping watchdog
(bornhack drops ICMP to the gateway, so it false-fired and bounced a
working connection every 90s).
Mark wlan links unmanaged by systemd-networkd so NetworkManager is
the sole wifi DHCP client. With networking.useNetworkd enabled, the
auto-generated 40-<iface>.network ran a second DHCP client on the
wifi; the two fought over the lease and routes and dropped the link
every few minutes. Match by Type=wlan so it holds across interface
renames.
The list_add corruption / lib/list_debug.c:32 freezes on WiFi roam were
an add-side race: generic mt76_sta_add() re-ran mt76_wcid_init() on a
wcid that mt7925_mac_sta_add had already published, letting the rx/txs
softirq link it into sta_poll_list right before the second
INIT_LIST_HEAD self-looped the node. Our local teardown-side patch
could not close that window (crashed again 2026-07-07, 4x in 11 min).
Upstream fix 20b126920a25 ("wifi: mt76: add wcid publish check in
mt76_sta_add") is in 7.1.3, which linuxPackages_latest now provides
after the nixpkgs bump. Keep kernel >= 7.1.3.
Six hard freezes on 2026-07-03, all during WiFi roaming between hoelle
APs. Captured oopses show identical list_add corruption in an 8k slab
(ieee80211_sta + drv_priv) -> kernel BUG at lib/list_debug.c:32 in
softirq context -> full system freeze.
Root cause: mt792x station teardown never clears wcid->sta, so the
rx/txs softirq path can re-add a dying wcid to sta_poll_list after
mt76_wcid_cleanup() unlinked it. Once the sta is freed and the slab
slot reused, mt76_wcid_init() self-loops the still-linked list node;
the next list_add_tail() panics. Also fixes the unlocked
list_first_entry read in mt7925_mac_sta_poll (mt7921 already takes
the lock there).
Full analysis in the patch header. Drop once fixed upstream
(https://github.com/zbowling/mt7925 tracks related work).
Also drop amdgpu.mes=0: the parameter no longer exists on 7.x
("amdgpu: unknown parameter mes ignored").
Matrix is a Tier-2 platform whose default tool-progress preview caps
commands at 40 chars, so a `terminal` call only surfaced e.g.
`curl -X POST -H "Content-Type: applic...`. Set the matrix platform's
tool_progress to "verbose" so the gateway renders the full, multi-line
command as a fenced code block.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The Hermes Agent ships a "claude-code" skill that delegates coding via
`terminal(command="claude -p '...'")`, but the `claude` binary wasn't on
the agent's PATH. Add it through services.hermes-agent.extraPackages, which
lands in both the systemd service PATH and the hermes user's profile.
Auth is separate: run `sudo -u hermes -H claude` on the host once.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The flock around `ipfs add --nocopy` in the watcher and reconcile sweep
guarded against a corruption that was assumed, never observed: it was
added defensively alongside the reconcile sweep and never exercised
(the MFS deadlock masked it).
Tested it directly on kubo 0.40.1: 10 trials of two concurrent
`ipfs add --nocopy --pin` on the same 16MB file, no lock. Every trial
produced the same CID, a complete DAG (refs -r), and byte-identical
content (cat | sha256sum). Concurrent same-file --nocopy adds are
idempotent, so the lock guards nothing -- remove it.
kubo 0.40.1 has a daemon-side MFS goroutine deadlock: the first
`ipfs files` write after each daemon start permanently wedges the global
MFS lock, so even a trivial `files mkdir` then hangs forever. This
silently parked the pin-watcher on its startup `files mkdir` and stopped
auto-pinning for ~4 days. It is not fixable client-side (timeout/flock/
serialize cannot release a lock held by a wedged daemon goroutine).
Rip out the browse-index entirely rather than gate it behind a toggle:
- drop the publish oneshot + timer (IPNS publish of the MFS root)
- drop all `ipfs files` ops from the watcher and reconcile, plus the
mfs()/mfs_add/mfs_rm/mark_dirty/relpath helpers, IPNS keygen, startup
mkdir, backfill, and the dirty-flag/mfs-lock machinery
- rename flix-index-reconcile -> ipfs-pin-reconcile (it is pin-only now)
Pinning is unaffected: the inotify watcher still `ipfs add --nocopy
--pin`s under the pin-add flock, and the hourly forward-sweep reconcile
still heals dropped inotify events.
The pin-watcher loop is single-threaded and blocks 1-2 min on each multi-GB
`ipfs add`. While blocked, a concurrent upload's CREATE,ISDIR + MOVED_TO for a
brand-new game subdir can overflow the inotify queue and be dropped, leaving a
fully-written file that is never pinned (gateway 504s on it forever). The
hourly reconcile only did reverse cleanup (drop entries for vanished files), so
a dropped event was permanent until a watcher restart re-ran the startup sync.
Add a forward sweep to the reconcile: pin on-disk files older than 10 min that
are missing from cid-map. Guard both the watcher's add_file and the sweep with
a shared flock (pin-add.lock) so two concurrent --nocopy adds of the same file
can never corrupt the filestore DAG. flock auto-releases on process death/OOM/
reboot, and an add interrupted mid-pin never writes its cid-map line, so the
sweep re-adds and completes it on the next pass.
Run Nous Research's Hermes Agent as a native systemd gateway on coaxmetal,
reachable as @hermes:lassul.us over Matrix (E2EE). Wires upstream's
nixosModules.default via a new flake input.
Backend is Nous Portal (one key, 266 models) driving an agentic model
(MiniMax M2.5) through the generic OpenAI-compatible provider — hermes's
built-in nous provider is OAuth-only against a dead endpoint, and Nous does
not recommend the Hermes-4 chat models for the agent. Secrets (Matrix access
token, Nous API key) come from clan vars; a startup DNS-wait guards the
IPv6-only homeserver against a boot race.
Assisted-by: Claude Code (claude-opus-4-8[1m])
A room's unread in cinny is the main-timeline notification count plus the
sum of every thread's count (Room.getUnreadNotificationCount sums
threadNotifications), and roomHaveNotification keys off that. The
room-level Mark-as-Read sent a receipt only for the latest non-thread
event, clearing the main timeline but leaving every thread count intact -
so a room with any unread thread stayed bold in the sidebar even after
being marked read, with no per-thread indicator to explain why.
Send an unthreaded read receipt against the room's latest event instead;
the SDK/server treat it as clearing the main timeline and every thread's
unread bucket at once.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Nix store files have mtime=1970 and the vhost set no Cache-Control, so a
browser can heuristically cache index.html for a long time and not pick
up a newly deployed build on reload. Serve index.html with no-cache
(revalidated cheaply via etag) and the content-hashed /assets/ with a
long immutable max-age.
Assisted-by: Claude Code (claude-opus-4-8[1m])
markThreadAsRead sent the receipt against thread.events[last]. Two ways
that fails to clear the thread server-side:
- The SDK's threadIdForReceipt() routes a receipt for the thread root,
or a relation to it (e.g. a reaction on the root), to the "main"
timeline, which never touches the thread's unread bucket.
- Receipting an event that is not the thread's true latest reply leaves
a residual server-side count, so the notification reappears on the
next sync/reload even for threads that were opened.
Target thread.lastReply() - the SDK's canonical latest m.thread reply,
taken from the authoritative thread timeline (including the bundled
latest_event from /threads) and the same event the SDK uses for its
read-up-to comparison. Fall back to a newest-first scan of the live
timeline / rendered replies only when the Thread object is not hydrated.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The thread unread badge in RoomViewHeader only recomputed on
ThreadEvent.New/Update/NewReply. Marking a thread read emits
RoomEvent.Receipt / RoomEvent.UnreadNotifications instead, so the badge
kept a stale count until unrelated thread activity fired a recompute -
the notification appeared persistent and laggy. Subscribe the badge
effect to those receipt events too.
Assisted-by: Claude Code (claude-opus-4-8[1m])
nixpkgs 26.11 systemd.nix now reads environment.variables.SYSTEMD_XKB_DIRECTORY
and config.i18n.imperativeLocale, but extra-container's minimal module set
omits the modules that declare them, breaking the container-virtulus eval in
`nix flake check`. Add dummy options to the existing workaround block.
Assisted-by: Claude Code (claude-opus-4-8[1m])
bitwarden-desktop pulls electron_39, which nixpkgs now marks insecure
(EOL), failing `nix flake check` (eval-xerxes). secureify it via overlay
rather than nixpkgs.config.permittedInsecurePackages, matching the
existing olm fix in mautrix-telegram (lib/secureify.nix).
Assisted-by: Claude Code (claude-opus-4-8[1m])
orange's autoswitch was failing activation: the git service's
mkdir /var/lib/git hit a symlink loop (a stale dangling
`state/git -> ../../var/state/git` link that loops because the container
bind-mounts both /var/lib and /var/state to the same state dir).
cgit is being retired, so remove the services/git import entirely. This
also unblocks orange's autoswitch (it no longer touches /var/lib/git).
Assisted-by: Claude Code (claude-opus-4-8[1m])
A transient GitHub 5xx while resolving the flake at boot used to leave a
container's autoswitch in a failed oneshot state with no retry, silently
running stale config (observed on yellow: the disk-space guard never
updated to the 100 GiB threshold).
Add Restart=on-failure with RestartSec=120s. Since RestartSec is well
above the default StartLimitIntervalSec, the restart rate limiter never
trips, so a transient failure keeps retrying every 2 min and self-heals
once GitHub recovers.
Assisted-by: Claude Code (claude-opus-4-8[1m])
ollama is no longer used here; removing it frees ~4G.
Prepare a fresh libvirt guest for riot on a dedicated public IP
(95.217.192.52, separate MAC) bridged directly onto ext-br. Because the
bridge is unmanaged, libvirt installs no iptables rules for it, avoiding
the nat-chain race that forced the old restart-iptables hack in hfos.nix.
- physical.nix: enable the ext-br bridge, enslave enp35s0, host IP on bridge
- config.nix: enable libvirtd, add riot login user, FORWARD accept for the VM IP
Bridge cutover applies at the next reboot window; the VM is created
imperatively afterwards.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Cinny has no thread overview UI, which makes the Zulip bridge's stream rooms
(one Matrix thread per Zulip topic) hard to navigate. Carry smokku's
"threads-ui" thread drawer/browser (PR cinnyapp/cinny#2787, closed only
because Cinny froze PRs during their SDK rewrite) as a patch over the
packaged v4.12.2 release and serve it at the matrix.lassul.us root.
The patch is the squashed feature diff resolved against v4.12.2; the only
conflict was RoomViewHeader.tsx (additive, alongside the call button). The
lockfile is unchanged so nixpkgs' npmDepsHash still applies. Re-resolve the
patch against the new tag on each cinny bump.
Assisted-by: Claude Code (claude-opus-4-8[1m])
Bridge Matrix to Zulip via GearKite/MatrixZulipBridge (packaged in nixpkgs
as matrix-zulip-bridge). No upstream NixOS module exists, so the systemd
service mirrors the heisenbridge module it is forked from: generate the
registration, merge in the tokens, and wire it into Synapse. Zulip orgs
(e.g. zulip.geninf.io) are configured at runtime via the bridge admin room.
Assisted-by: Claude Code (claude-opus-4-8[1m])
The security.dhparams module was removed from nixpkgs (RFC 7919), but
HedgeDoc's HTTPS listener unconditionally readFileSync()s dhParamPath at
startup. With the path unset it read a directory (EISDIR) and crash-looped,
taking pad.lassul.us down. Point dhParamPath back at the existing file and
add an ExecStartPre that regenerates it with openssl if missing.
Assisted-by: Claude Code (claude-opus-4-8[1m])
/var/download churns constantly (download temp files appearing and
vanishing), so `find` exits non-zero when an entry disappears between
readdir and stat. Under `set -efu` that aborted the oneshot, which during
a nixos switch counted as a failed unit and flipped the whole switch to a
non-zero exit. Tolerate find's exit code and only replace the live index
if a non-empty listing was produced.
Assisted-by: Claude Code (claude-opus-4-8)
When /var/download drops below 100 GiB free the guard now stops all
transmission torrents (it has no native low-disk pause like sabnzbd's
download_free) and posts a Matrix message to !MRjkvAaFPsjdYCpeiZ:lassul.us
via the archiver bot's access token. Torrents are restarted and a
recovery message sent once space climbs back above the threshold. State
is tracked in StateDirectory so alerts fire only on transitions, not
every run.
Assisted-by: Claude Code (claude-opus-4-8)
The previous guard only throttled sabnzbd below 100 GiB and paused at a
15 GiB floor, which still let the disk fill close to full. Use sabnzbd's
native download_free/complete_free (continuous pause + auto-resume) set
to 100 GiB so it pauses the moment free space drops below that, and have
the guard keep those configured. Transmission has no equivalent low-disk
guard, so the guard still throttles its downloads to a trickle below the
threshold while leaving seeding alone.
Assisted-by: Claude Code (claude-opus-4-8)
Recent wireguard-tools resolve the peer Endpoint in the namespace where
`wg syncconf` runs. The transmission netns has no route until the tunnel
is up, so resolving europe.vpn.airdns.org from inside it always fails;
wg burns ~70s of DNS-retry backoff per attempt before erroring. During a
nixos switch that restarts transmission-netns this stalled the whole
deploy for >2min and left the service (and the download stack) failed.
Resolve the endpoint in the main netns (which has connectivity, and is
already gated on the `host` check above) and hand wg a numeric Endpoint,
so the tunnel comes up instantly. Also fix the bare `sleep` in the DNS
wait loop, which errored under `set -e` instead of retrying.
Assisted-by: Claude Code (claude-opus-4-8)
Adds download-space-guard: a 5-min oneshot that caps sabnzbd and
transmission speeds once free space on /var/download drops below 100 GiB,
and pauses sabnzbd below a 15 GiB floor. This prevents a full disk from
hard-wedging sabnzbd with an unrecoverable "Disk full! Forcing Pause";
the guard auto-resumes once space frees up.
Assisted-by: Claude Code (claude-opus-4-8)
/boot is only 1 GiB (disk.nix). Without configurationLimit, GRUB retains
every generation's kernel+initrd until the ESP fills, at which point the
bootloader install fails ("No space left on device") and deploys can't
finish. Keep the 10 most recent; older kernels get pruned automatically.
Assisted-by: Claude Code (claude-fable-5)
The whole system lives on one btrfs-on-LUKS-on-NVMe device, so downloads
and builds were starving latency-sensitive reads — launching a terminal
would freeze for seconds (PSI full IO pressure spikes, near-zero CPU/mem
pressure). Three independent fixes:
- mq-deadline scheduler on nvme0n1: the default "none" gives no read/write
fairness, so reads queued behind up to 1023 writes.
- vm.dirty_bytes/dirty_background_bytes as small fixed values instead of
the ratio defaults (~6/12 GB on 62 GB RAM) that caused multi-GB flush
bursts.
- bypassWorkqueues on the LUKS device to drop the kcryptd serialization
point (safe: AES-NI is present).
Assisted-by: Claude Code (claude-fable-5)
The clan bump removes the `clan.core.vars.generators.<name>.migrateFact`
option, which made every host fail to evaluate. Fact migration is a
one-time step that's long complete, so drop the now-removed option from
all generators (retiolum, syncthing, shadowsocks, hedgedoc, mycelium,
hotdog/orange containers, nextcloud, wiregrill, nix-serve, matrix).
Assisted-by: Claude Code (claude-fable-5)