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

124 lines
6.9 KiB
Markdown

# 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
&hellip; 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
- [MatrixReactionsTest "the target is the newest own message that contains the text"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactionsTest.kt)
- [MatrixReactorTest "a message gives its text, an edit none"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactorTest.kt)
- [MatrixReactorTest "an encrypted event gives what its cleartext shows, a decrypted one its content"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactorTest.kt)
- [MainTest "matrix react takes room, text and key, in any order of the options"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/MainTest.kt)
#### Scenario#91.02: a reaction that is there already stays single
&hellip; 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
- [MatrixReactionsTest "an own reaction with the key, with or without U+FE0F, is present"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactionsTest.kt)
- [MatrixReactionsTest "a reaction of another user, with another key or to another message is not present"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactionsTest.kt)
- [MatrixReactorTest "a reaction gives the message it annotates and its key"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactorTest.kt)
- [MatrixReactorTest "a reaction in the outbox counts as the user's, by its event ID once it is sent"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactorTest.kt)
#### Scenario#91.03: no matching message
&hellip; 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
- [MatrixReactionsTest "without an own message that contains the text there is no target"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactionsTest.kt)
- [MatrixReactionsTest "each outcome prints one line, and only a missing message ends with exit code 1"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactionsTest.kt)
#### Scenario#91.04: no reaction while the relay syncs
&hellip; 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
- [MatrixReactorTest "while another process syncs, the command refuses and says how to stop the relay"](../../dashboard/src/test/kotlin/de/hoennig/werkregie/dashboard/sources/MatrixReactorTest.kt)
## 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.