Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
Make every bot response a thread reply instead of a fresh top-level
message. send_notice gains a thread_root kwarg that adds m.relates_to
with rel_type=m.thread plus the m.in_reply_to fallback. on_message
picks the thread root via thread_root_for(): if the user's message is
already inside a thread we reply into that one, otherwise we start a
new thread rooted on their message.
For Radarr/Sonarr download notifications to thread back to the
original request, tracked_requests.json now stores the request's
event_id alongside the room. load_tracked normalizes legacy bare-
string entries so existing tracked items keep working (just without
threading on the eventual notification).
Three problems were stacked on top of each other:
1. matrix-nio's client.join doesn't expose ?server_name=, so when our
homeserver hadn't fully processed the federation invite by the time
we turned around to join, it returned M_FORBIDDEN with no retry.
2. Even with retry, on_invite only fires once per sync delivery; if
joining failed we never tried again.
3. client.invited_rooms is populated incrementally from the persisted
next_batch, so synapse won't re-send invites that arrived in a prior
session, which means the startup loop was a no-op for stuck invites.
Add a try_join helper that calls /join/{room_id}?server_name=<inviter>
directly via _send and retries with exponential backoff, and a
fetch_pending_invites helper that does a fresh /sync (no since token)
to ask the server for the real invite set. Use both in on_invite and
in the startup loop.
MegolmEvent has no `.message` attribute (the real field is `.ciphertext`),
so the on_megolm callback added in 54d7388 raised AttributeError on the
first undecryptable event and took the bot down. Drop the field from the
log line — sender + session_id are enough.
Also re-raise after logging in the sync_forever wrapper so the process
exits non-zero and systemd's Restart=on-failure actually triggers; the
swallowed exception was why the bot stayed dead for 5 days instead of
restarting.
The previous translation of the dovecot 2.3 plugin/namespace blocks didn't
survive doveconf parsing or the runtime check for virtual namespaces:
- `acl = vfile` is a named-list-filter in 2.4. The driver moved to
top-level `acl_driver`, and per-identifier defaults live in
`acl <identifier> { acl_rights = ...; }` filters keyed on the ACL id
(owner/anyone/user=...). The filter name itself is parsed by
acl_identifier_parse, which is why earlier attempts with `acl vfile`
or `acl primary` failed with "Invalid identifier".
- `acl_shared_dict` was removed; the bot subscribes to shared paths
explicitly so we don't need it.
- Per-namespace `location = "driver:path:opts"` was split into separate
mail_driver / mail_path / mail_index_path settings.
- The virtual namespace also needs `mailbox_list_layout = fs` to override
the global Maildir++ - otherwise dovecot 2.4 refuses to initialise the
namespace, which broke both LMTP delivery and IMAP login.
Verified on neoprism with `doveconf -n` and a live LMTP delivery.
Dovecot 2.4 (now in nixpkgs) removed services.dovecot2.extraConfig and
mailPlugins. Translate the plugin/namespace blocks into the structured
settings tree, hoist ACL plugin keys to top-level (no more `plugin {}`
section), and update the variable-substitution syntax (%n -> %{user |
username}, %u -> %{user}).
Also drop the explicit master ref on nixos-mailserver since the project
no longer uses that branch name as default tracking.
claude is already provided via tools.s (and the default tools include
s now), so drop the explicit claude entry and add s for direct access.
Use the new wrapper plugins option to install the GouvernAI plugin from
GitHub, providing the /guardrails command and gouvernai flow-state skill.
Allow declaratively adding extra Claude Code plugin directories alongside
the bundled one. Each entry becomes a repeatable --plugin-dir flag.