Files
werkregie/doc/prs/2026-10-16-PR#91-matrix-react.md
claude-code 6bba77a82f
werkator build succeeded after 01:01
werkator/deploy build succeeded after 00:24
An agent marks a Matrix message with a reaction (#91)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-16 16:13:07 +02:00

6.9 KiB
Raw Permalink Blame History

PR#91: An Agent Marks a Matrix Message with a Reaction

Version: minor

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR documentation is not maintained along with new PRs; treat it as a snapshot.

Problem

The user announces pull requests of hs.hsadmin.ng for review in the Matrix room "HSAdmin NG". An agent merges the approved ones and must then mark each announcement: ✅️ when the pull request is approved, ☑️ in addition when it is merged. The room is end-to-end encrypted; werkregie has a verified device of the user's account, but only reads.

Non-Goals

  • No messages, replies or edits; only a reaction (m.reaction, rel_type: m.annotation).
  • No removal of a reaction.
  • No reaction to messages of other users.
  • No HTTP endpoint for it; the command line only.
  • No reaction through the running relay; see "Follow-up PRs".

Specification

Feature: werkregie-dashboard matrix react marks the newest own message that contains a text

Scenario#91.01: an announcement gets the reaction

… so that the room shows which pull request is approved.

  • Given the user's newest own message in the room "HSAdmin NG" that contains /pulls/329, without a reaction ✅️ of the user
  • When an agent runs werkregie-dashboard matrix react --room "HSAdmin NG" --contains /pulls/329 ✅️
  • Then the message has the reaction ✅️ of the user, and the command prints REACTED with the event IDs
Verified by

Scenario#91.02: a reaction that is there already stays single

… so that an agent can repeat the command after a failure.

  • Given the message has a reaction ✅️ of the user, with or without the variation selector U+FE0F
  • When the agent runs the same command again
  • Then nothing is sent, and the command prints PRESENT
Verified by

Scenario#91.03: no matching message

… so that a wrong text does not mark another message.

  • Given no own message among the last 500 events of the room contains the text, e.g. only /pulls/3290 for /pulls/329
  • When the agent runs the command
  • Then nothing is sent, and the command prints NO-MESSAGE and ends with exit code 1
Verified by

Scenario#91.04: no reaction while the relay syncs

… so that two clients never use the device's store and keys at once.

  • Given the relay of the machine holds sync.lock and runs the Matrix client
  • When the agent runs the command
  • Then nothing is sent, and the command says to stop the relay first and ends with exit code 3
Verified by

Solution

  • MatrixReactions decides, free of the client library: the room by its ID or name, the newest own message whose text contains --contains, not followed by a letter or digit, and whether an own reaction with the key is there already; U+FE0F does not count, since clients differ in it.
  • MatrixReactor takes sync.lock, opens the device's client, waits for the first sync, reads the last 500 events of the room backwards, decrypted where the device has the key, and sends m.reaction. Trixnity encrypts it in an encrypted room. A reaction of an earlier run that still waits in the client's outbox counts as present, so it is not sent twice.
  • Exit codes: 0 for REACTED and PRESENT, 1 for NO-MESSAGE, 2 for a wrong call, 3 for a failure, as watch-pr.
  • No token or key is printed: the command prints event IDs only, an error its message.

Why the command takes sync.lock rather than going through the running relay: the relay holds the lock as long as it runs, and only MatrixSource has its client. A control socket in MatrixSource would make pitestPartial mutate all of its methods; MatrixSource.message alone has a Mutation-CRAP of 812, far above the limit 15 of a branch. So MatrixSource needs its refactoring first, in a PR of its own. Until then the agent stops the relay for the reactions and starts it again; the dashboard then misses the relay's report for that time and says so. A relay takes the lock only with its first fetch, a few seconds after its start; until then matrix react gets the lock, too.

Checked live in the user's test room, with the relay stopped: REACTED, then PRESENT for the same call; NO-MESSAGE with exit code 1 for a room found by its name. A call takes about 50 s, mostly the first sync since the relay last ran.

Release Notes

  • werkregie-dashboard matrix react --room RAUM --contains TEXT EMOJI marks the newest own message with a reaction; a running relay or local mode stops for it.

Open Questions

None.

Follow-up PRs

  1. A refactoring of MatrixSource: each of its methods at a Mutation-CRAP of at most 15, the limit of pitestPartial for the classes a branch changes.
  2. Then the relay that holds sync.lock sends the reaction for matrix react through a control socket; the relay no longer stops for it.