Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
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])