Commit 4fa765e
authored
feat: live room awareness — daemon, event log, reader (#88)
Design, plan, and implementation of live room awareness: a daemon owns
the E2EE store and streams decrypted room events to a per-room log, so
an agent can follow a room while it works, post its own status, react,
and answer without blocking.
## What decides the shape
One process may hold the E2EE store. Two nio processes on one store
corrupt it — this repository's own store carries a `*.db.corrupt-<date>`
from exactly that. And sync is account-wide: whichever process syncs
consumes the to-device events, so room keys land wherever the race puts
them. Both were hit during the work behind this branch, and both
presented as something else — one as `BAD_ACCOUNT_KEY`, one as a
verification that would not complete.
So one daemon owns the store and syncs. It appends decrypted events to
`rooms/<slug>.jsonl`. Readers tail that file, touch no store, and can
run any number at a time. Send, react and redact ask the daemon's socket
first and fall back to today's direct path when nothing answers — no new
flags, no second way to send a message.
**The routing signal is the socket, not the lock.** A direct send holds
the same lock for its couple of seconds, so a command starting in that
window would conclude "a daemon is running" and talk to a socket nobody
serves. The lock answers whether the store may be opened; only the
socket answers whether there is someone to delegate to.
## What is here
`docs/specs/2026-08-13-live-room-awareness.md` — the design, in
[OKF](https://okf.md/) with an index, which is the format this
repository uses for knowledge from here on. The event log stays JSONL
and the spec argues that from OKF's own scope: one concept per file, no
provision for streams, and its `log.md` is newest-first, so every append
would rewrite the file.
`docs/exec-plans/completed/2026-08-13-live-room-awareness.md` — the
plan, all nine tasks ticked, with a status section on what the
implementation added that the plan did not anticipate.
`_lib/roomlog.py`, `_lib/daemon_client.py` — stdlib, 44 tests.
`matrix-watchd.py` — the daemon. `matrix-watch.py` — the reader. The
three commands gain a routing branch.
## Defects found by reviewing and by running
Five in the spec, before any code:
- Routing decided on the store lock. Fixed to decide on a successful
connect.
- The daemon's concurrency was unspecified; a sync is a 30-second long
poll, so serving the socket between syncs would delay a send by that
much. Now two tasks on one loop.
- The cursor stored an `event_id`, which rotation can carry away,
leaving nothing to count from. Records now carry a monotonic `seq` and
counting is subtraction.
- `mentions_me` was left for readers to re-derive from a display name
they would have to look up.
- The reader piped through `jq` — the first external binary the skill
would have required.
Four in the plan, before implementing: a timezone-dependent test
assertion, `Path.with_suffix(".jsonl.1")`, a step that used
`rooms_dir()` before the step creating it, and an import inside a loop.
Three by running the code:
- `XDG_RUNTIME_DIR` is exported on this machine for a `/run/user/1001`
nobody created. The daemon died in `mkdir` before doing anything.
`socket_path()` now tests the directory rather than trusting the
variable.
- `write_room_bundle` in the daemon was untestable only because the
module imports nio. Moved to `_lib`, six tests on the OKF shape.
- SKILL.md documented `matrix-redact.py ROOM '$eventId' "reason"`; the
script takes `--reason`, so the documented form has always exited with
"unrecognized arguments".
## Verification
44 tests across `roomlog`, `daemon_client` and the existing suites, all
passing, and they run in CI since #84.
Exercised live: the reader against a synthetic log (summary, backlog,
cursor, and records appended while following); send, react and redact
against a fake listener at the socket, each delegating and never opening
the store; the same commands with the socket removed, falling through to
the direct path; and the daemon's `--status`, `--stop`, its refusal with
no `watch_rooms`, and its store open.
**Not verified here:** the sync loop. This machine's store is on the
vodozemac backend while the scripts pin `matrix-nio[e2e]<0.26`, so
opening it reports the backend mismatch from #85 instead of syncing.
That is a pre-existing condition of this store, not of this change — and
the diagnosis appearing is itself evidence that path works. Anyone
running this needs a store on the pinned backend, which is the migration
the E2EE guide documents.
## Decisions with an owner
Recorded in the spec rather than left implicit: the agent may reply on
its own, without a preview step, into a room where colleagues read; and
no room traffic is filtered, at a context cost the one-line `text` field
reduces but does not remove. Both were asked for explicitly.18 files changed
Lines changed: 2993 additions & 10 deletions
File tree
- docs
- exec-plans/completed
- specs
- skills/matrix-communication
- references
- scripts
- _lib
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
8 | 12 | | |
9 | 13 | | |
10 | 14 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
103 | 104 | | |
104 | 105 | | |
105 | 106 | | |
| |||
0 commit comments