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