Skip to content

feat(matrix): expose explicit pin and unpin actions - #125882

Draft
iainlane wants to merge 138 commits into
NousResearch:mainfrom
iainlane:feat/matrix-pins
Draft

iainlane wants to merge 138 commits into
NousResearch:mainfrom
iainlane:feat/matrix-pins

Conversation

@iainlane

@iainlane iainlane commented Sep 28, 2026 •

Copy link
Copy Markdown

Stack

All campaign PRs in this section target main. This list records logical merge order. Nesting means the parent PR must merge before its child. Every prerequisite for a dependent must merge first. Only adjacent layers are shown; each linked PR description lists its other dependencies.

Summary

This PR adds matrix_pin with explicit pin and unpin actions for the current Matrix room. It changes the room's m.room.pinned_events state through the adapter and client that received the current message.

The tool is in a new matrix_admin toolset, which is restricted to Matrix and disabled by default. Enable it for Matrix in hermes tools. The desktop Toolsets page hides it, as it hides the Discord toolsets, because the switch has no effect on desktop sessions.

Permissions

Two checks apply to every change:

  • The requesting user must be a room version 12 creator or have the power level required to send m.room.pinned_events. This stops a member from using the bot's power to change pins that they could not change themselves. The level is read from the raw power-levels state with the spec defaults, including a state_default of 0 when the room has no power-levels event. When the requester's level cannot be determined (no power levels and a create response without a creator), the change is refused. A refusal reports the required and actual levels.
  • The homeserver then decides whether the bot may send the state event. A rejection is returned as M_FORBIDDEN with the server's message.

The change runs through the room-inspection access checks. Before the change and around every state read (power levels, encryption, the create event and the pinned events), the adapter checks that the room is still joined and allowed, that it is still the same direct chat or group room, that the requester is still authorized, and that the receiving client has not been replaced or closed. If any of these changes while a read is in flight, the change is refused and nothing is written.

Pin state

The update keeps the room's pin order and the other fields of the state content. Pinning an event that is already pinned, or unpinning one that is not, reports unchanged without a write. Redacted pin state ({}) and a null pinned value are read as an empty list, as Element reads them. Entries that are not event IDs are written back unchanged and left out of the reported list. A pinned value that is not a list is refused. Concurrent pin changes through one adapter are serialized. Matrix has no conditional state update, so another client's concurrent state write can still race with this one.

Interruption and unknown outcomes

Interrupting the requesting turn, the tool's 30-second deadline, or cancellation stops the pending work on the gateway loop, and the tool waits until the owning task has stopped before it reports. If the state write may already have reached the homeserver, the result says that the outcome is unknown and tells the model to read the current pins with matrix_read kind=pins before retrying. This also applies when the write's own 10-second timeout expires or the connection fails after the request was sent. A Matrix error response is a definite failure.

Documentation

The toolsets and tools references list matrix_admin and matrix_pin, and the Matrix guide describes the toolset and both permission checks.

Review scope

The PR targets main. Merge the prerequisites in the Stack section first. The signed linear history preserves the original author and committer identities, dates, timezones and complete messages, including contributor trailers.

Review the own layer with the scoped comparison.

Previously published main-loop fixture correction

The synchronous registry refusal test now closes the main tool loop that the test creates. It preserves any loop that existed before the test. The actual registry dispatch, every assertion and all parameter cases remain unchanged. This correction is folded into the original pin feature contribution. All four pin contributions preserve their original author and committer identities, dates, timezones and complete messages. That fixture correction changed only the pin test module.

With the same runtime composition, the original complete pin tool file passes 31 cases but reports one unclosed event loop and two unclosed sockets after the test summary. The corrected complete file passes the same 31 cases. Full child output through interpreter exit reports no unclosed database, event loop, socket or transport. Both executions use the actual SDK, all four runtime and resource warning errors, and zero retries. The SDK deprecation warning remains visible.

The runtime composition applies the two exact published worker-loop corrections from #135482. Those corrections are a prerequisite and are not duplicated in this branch's four pin contributions. The affected test module has zero type diagnostics before and after the correction. Ruff passes. Its existing formatting differences remain. This is a fresh one-file fixture gate. The Historical verification section below describes its recorded earlier revisions.

Standalone worker fixture lifetime

The executor-thread test dispatchers now close their worker event loops in the same thread after dispatch returns or raises. The complete helper is the same helper already published in the native poll tests. Every pin worker dispatch uses the helper. The existing main tool-loop cleanup, cancellation ordering, assertions and parameter cases remain unchanged. That fixture correction changed only the pin test module. This correction is folded into the original pin feature contribution, with all four original author and committer headers and complete message prefixes preserved.

The standalone current public pin file reproduces eight failures caused by strict event-loop and socket resource warnings, with 23 passing cases. The corrected complete file passes all 31 cases. Its full child output through interpreter exit reports zero database, loop, socket, transport or client resource warnings. Both complete-file executions use the actual SDK, all four strict warning errors and zero retries. The historical composition with the separate #135482 worker-lifetime changes also passes 31 cases with no resource warnings; that composition previously masked this standalone fixture dependency. Those core changes remain separate prerequisites.

The affected complete module has zero type diagnostics before and after this correction. Ruff passes. The same inherited formatting differences remain. This is a fresh standalone one-file fixture validation. The checks recorded below retain their earlier source qualifications. That standalone fixture successor preserved its then-current branch base. The current-upstream compatibility of this revision is described in the section below.

Historical verification

The canonical test runner passes 145 cases across three complete pin/inspection files. All gates use one worker, no retries and strict runtime, unraisable-exception and unhandled-thread warnings.

Ruff passes across the 53 changed Python paths in the six-layer source scope. The complete type comparison reports 410 existing diagnostics, down from 425; each remaining diagnostic is correlated with the original or accepted prerequisite source. Existing formatter differences remain recorded. The caption-only expectation correction preserves all reported source expressions. No global type-clean or formatter-clean result is claimed.

The focused Desktop toolset-curation implementation, direct helpers, manifests and renderer test configuration are unchanged. Earlier Desktop test counts are historical description evidence; they are not represented as a fresh global Desktop check.

Current upstream compatibility and complete fixture lifetime

This revision composes the corrected Matrix context prerequisite with the original inspection and pin contributions against upstream 3637c512fc3211bdccb61b7140d09ac1018508ba. It retains the original feature-contract contribution as an explicit empty credit record. The current prerequisite supplies its source and preserves the previously reviewed contributor metadata and fixture maintenance.

The inspection prerequisite now includes the published same-thread worker-loop teardown from #125732. All seven dispatch calls use that helper, and their original assertions and cancellation behaviour remain unchanged. The pin tests retain the published five worker wrappers and main-loop teardown from #125882. The bounded catalogue and adapter joins preserve the current pin feature alongside upstream toolset selection and timeout annotations.

The current complete-file validation comprises 374 assertions in seven files, with no database, event-loop, socket, transport or client shutdown warnings. All seven complete files were freshly exercised on the recorded final source, including all 17 read-tool assertions. The earlier failed read-tool child is preserved as the reproduced control and is excluded from this passing scope. All four strict warning categories were enabled and file retries were disabled.

The complete 21-path type comparison contains 144 inherited diagnostics, with no introduced occurrence compared with the original 151 and prepared prerequisite 140. Ruff reports no findings. The 19 existing formatter paths remain inherited. Earlier validation sections describe their recorded historical source and do not claim a fresh rerun on this revision.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets comp/plugins Plugin system and bundled plugins platform/matrix Matrix adapter (E2EE) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation labels Sep 28, 2026
Matrix permalinks change with each triggering event. Rendering one in
the pinned session prompt made the prompt stale or changed its bytes
every turn, which breaks conversation prompt caching.

Put the link in the current model-facing user message and remove it from
the durable transcript, including queued turns. Respect effective PII
redaction when adding the note. Percent-encode room IDs, event IDs and
the via server so delimiters cannot change the matrix.to URL structure.

Adopt the published upstream source-note prefix removal when
composing this prerequisite with current upstream. Keep the complete
Matrix and Discord transport selection, prompt-caching contract and
durable-transcript behaviour. The same removeprefix mechanism is
already published by the original source-note producer.
The live Matrix exchange now checks the model request for a permalink to
the event sent by the independent client. It also verifies that the link
is absent from the system prompt, which must remain stable across turns.
Record the exchange duration separately from container setup time.
Room version 12 IDs have no server component. A permalink built only
from the room ID therefore omitted the server hint, which can prevent a
client outside the room from locating it.

Use the joined bot account's server when the room ID has no domain. The
builder still leaves via absent when neither server is known.
A room ID can identify a server that no longer participates in the
room. Its server may also include a port or IPv6 address, which the
last-colon parser truncated to an invalid via hint.

Use the joined bot account's server for inbound source links. When only
a room ID is available, parse its full server after the first colon.
The same parse also corrects the room's session scope.
In a thread, every turn's source link pointed at the thread root. The
root was chosen as a stable anchor while the link lived in the pinned
session prompt, which is per thread. The link now goes on each user
turn, so the reason no longer applies, and the model never received the
ID of the message that triggered the turn. A request to cite "this
message" produced a link to the root, which may be another user's
message.

Build the per-turn link from the triggering event in all cases. A
matrix.to link to an event inside a thread opens that thread in current
clients, and the session prompt already gives the thread root.
A room-ID permalink needs via servers so that a reader outside the room
can join through one of them. The link carried only the bot's server.
When that server was unreachable from the reader's server, the link
failed.

Choose up to three servers as the Matrix specification recommends for
room-ID permalinks. The first is the server of the highest-power joined
member at power level 50 or above; room version 12 creators count as
the highest power. The rest are the servers with the most joined
members, and IP literals are never chosen. The adapter reads members,
power levels and the create event from the mautrix state store, which
sync keeps current, so building a link makes no request. When the
store has no joined members for the room, use the bot's server and then
the room ID's server.

Server ACLs are not applied, because the state store does not keep
m.room.server_acl. Move the builder and the selection into a sibling
module, and emit one via parameter per server.
The session prompt stopped rendering the Matrix source link when the
link moved to the current user turn. Two prompt tests still asserted
that the prompt omitted the link when it was unset or redacted. No code
path produces that output any more, so neither test could fail because
of this feature. The prompt-stability test covers the remaining
contract, and the per-turn tests cover the note and its PII
suppression.

Restore the comment on the per-turn notes. strip_inbound_source_note
removes a note only when it is the first thing in the message, so a
later prefix added outside the notes would leave the note in the
persisted transcript.
Choosing source-link via servers made every inbound message await
get_power_levels and get_create on the client's state store. The
project-context isolation and voice tests gave the adapter a MagicMock
store with only get_members awaitable, so all six inbound tests in
those files failed with "'MagicMock' object can't be awaited".

Production always uses mautrix's MemoryStateStore. Give both fakes a
real MemoryStateStore in which the bot and the sender have joined the
test rooms. The real store implements every state query, so these tests
keep working when the adapter starts reading other room state. The
stores are filled with MemoryStateStore.deserialize because the helpers
are synchronous and some are called inside a running event loop.
The effective-state parent added a description to the matrix_read limit
argument: "Maximum number of events to read". In this layer the same
argument also bounds the members kind, which returns members rather
than events, and the pins kind. Say that the limit applies to events or
members so the model can tell that it bounds member lists too.
The merged matrix_read description said that events are listed oldest
first and that `errors` lists events that could not be decrypted. Both
sentences came from the history reads. A pins read returns events in
the order of the room's m.room.pinned_events list, and its errors also
cover pins that could not be fetched, are not visible messages, came
back as another event, or whose edit was redacted. A model that read
the description could tell a user that a deleted pin was an encrypted
message.

Limit the ordering sentence to room and thread reads, say that pins
follow the room's pinned order, and say that `errors` lists events that
could not be read or decrypted, with the reason. History reads already
returned non-decryption errors such as "event content changed", so the
wider wording is also accurate for them.
Room classification controls the Matrix room allowlist and mention
gates.
The previous member-count shortcut classified a bot-only room as a DM,
and an m.direct entry classified larger or unreadable rooms as DMs.
Those rooms could bypass both gates.

Classify a room as a private bot chat only when the bot and exactly one
other user are joined. Request JOIN membership explicitly: mautrix's
state store includes invited users by default. The state store may also
contain only a subset of a room's members, so use its joined IDs only
when has_full_member_list confirms completeness. Otherwise query the
homeserver with get_joined_members and apply room rules if that fails.

Keep m.direct as room identity metadata, without using it for
authorisation. Document the resulting allowlist and mention policy.

Tests reproduced the bot-only, bot-absent, multi-user, invited-user,
partial-cache and unknown-membership cases before their fixes.
The DM tests marked m.direct account data but did not supply joined
members. The two-person classifier therefore treated those fixtures
as rooms, so their threading assertions tested the wrong policy.

Give the fixture a full joined member list with the bot and one other
user. Keep the three-member variant for room behaviour. The mention
and threading tests now exercise the intended DM policy.
A room is a private bot chat only when the bot's user ID is one of its
two joined members. With an access token, the adapter replaces the
configured MATRIX_USER_ID with the ID that whoami returns. After a
password login it kept the configured string. Synapse accepts a login
identifier whose localpart differs in case from the registered user, so
a configured @Hermes:example.org logs in as @Hermes:example.org. The
membership check then never matched, and every one-to-one chat followed
room rules: with MATRIX_ALLOWED_ROOMS set its messages were dropped,
and otherwise they needed a mention.

mautrix's login stores the homeserver's user ID in client.mxid. Copy it
into the adapter after a successful login, as the whoami path does, so
the membership check uses the ID that appears in the room's member
list.
The Matrix handoff session-key test marked its DM case only through the
m.direct cache. Room classification no longer reads m.direct, so the
DM case was keyed as a group and the test failed. Give the fake client
a joined member list: the bot and one other user for the DM case, and a
third user for the room case.
The environment-variable reference still said that any room with two or
fewer joined members is a DM, and told operators to add a third member
to get room behaviour. A bot-only room, a room without the bot and a
room whose membership cannot be read now follow room rules. Describe
the current rule in the MATRIX_ALLOWED_ROOMS, MATRIX_REQUIRE_MENTION,
MATRIX_FREE_RESPONSE_ROOMS and MATRIX_AUTO_THREAD rows.

The comment in _schedule_pending_invite_joins said that a direct invite
missing from m.direct is misclassified as a group. m.direct no longer
affects classification, so drop that clause.
The E2EE crypto store keys every row (the Olm account, Olm sessions
and inbound Megolm sessions) by an account ID taken from the adapter's
user ID. After a password login the adapter now replaces the configured
MATRIX_USER_ID with the homeserver's spelling of it, so the DM check
matches the room's member list. That replacement also changed the store
key. A deployment with a password login, MATRIX_USER_ID differing in
case from the registered user, and a stable MATRIX_DEVICE_ID would open
an empty store after upgrading. It would create a new Olm account for
its existing device and could no longer decrypt history. The same
happened on an in-process reconnect, because the second login read the
replaced ID.

Keep the configured ID in its own attribute and use it for the login
identifier and, after a password login, for the store key, as before.
The access-token path keys the store by the whoami answer, as it always
has. The adapter's user ID still becomes the homeserver's spelling on
both paths, and the DM, mention, invite and self-sender checks use it.

The password-login reconnect fixture now disconnects its first real client session before opening the replacement, matching the adapter reconnect lifecycle and preserving every identity assertion.
Matrix rooms often have no m.room.name state event. Identity
resolution used the opaque room ID as the name shown to the agent
even when other occupants had readable display names.

Read member profiles from the state store, with the joined members
API as a fallback when the store is empty. Use the other occupants
to title an unnamed room. Explicit-name detection and DM
classification remain unchanged.
Matrix room identity was cached without reacting to live state
changes. Topic edits, renames and membership changes could leave
stale room context. The agent also missed important privacy or
replacement changes.

Register sync-awaited state handlers that invalidate the identity
cache. Queue one note per change kind for the next ordinary message.
Keep notes through mention drops, commands and text batching. Quote
and bound room-state values before they enter channel context.
Effective-event snapshots replace the earlier string history methods,
but the current prerequisites also exclude messages already included
in a merged turn and distinguish authored quotes from reply fallbacks.
Keep those boundaries in the extracted context mixin and shared readers.

Adapt the inherited contracts to the snapshot API and actual client
request binding. Their encrypted unit fixtures now provide the concrete
SDK decoder, and error results consistently contain string event IDs.
The adapted two-file gate passes 414 tests without retries.
The inbound pipeline is already larger than the project module limit.
Its cache transfer logic has separate filesystem and profile concerns
from the turn orchestration around it.

Move that existing logic into run_inbound_media before changing its
ownership policy. The pipeline and Matrix snapshots import the defining
module, and tests patch the same bindings that production reads. The
six selected ownership and quote contracts retain their behaviour.
A prepared launch-profile turn keeps the file from the quoted event
cache. Routing a later reply to another profile moved that same file
out of the launch cache, so the earlier turn lost its pixels before
model use. The retained event snapshot could not prevent a filesystem
move.

Copy quoted attachments into the routed cache and retain their source.
Publish each copy with an atomic rename so another prepared turn cannot
read a partial replacement. Authored attachments keep their existing
move policy.

The native and text quote contract now checks that every previously
prepared file remains readable throughout launch, routed, routed and
launch turns. Both successful-routing cases failed before the fix; all
four quote cases and the existing authored ownership contracts pass.
When an inbound cache transfer failed or its source file was missing,
the event kept the launch-profile path. Authored attachments then
reached media consumers in the routed profile, even though that profile
had not received the file. Quoted attachments were hidden from vision
but still occupied their original positions in the event.

Remove each failed transfer from the event before media preprocessing.
Preserve the caption and replace an inline reference to the failed file
with an unavailable marker. Rebuild the parallel media lists and quoted
dependency indices so valid siblings retain their original meaning.

The whole-event contract reproduces four failures on the unchanged
parent, with two existing ownership controls passing. All 797 tests in
the combined twelve-file effective-state and media gate pass after the
repair without retries.
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

The synchronous registry refusal test creates the process-owned tool
loop. Worker cleanup does not own that loop. Close the loop after the
asserted dispatch when this test created it, preserving any loop that
already existed before the test.


Pin fixture worker-loop lifetime correction:

The pin tests dispatch tools in executor threads. On the standalone
branch, those workers keep thread-local event loops alive until thread
exit, and their selector sockets produce strict resource failures. The
previous broad gate included the separate core worker-lifetime fix, so
that composed gate did not expose this fixture dependency.

Use the already published poll fixture worker dispatcher for every pin
worker call. It closes the worker loop in its owning thread after tool
dispatch returns or raises. Keep the existing main-loop cleanup, all
cancellation ordering, and every behavioural assertion unchanged. The
complete standalone pin file reproduces eight strict resource failures
before this correction.

Preserve the explicit pin feature when composing the published
context prerequisite with the current tool catalogue and Matrix
adapter. Keep upstream toolset selection and timeout annotations;
all existing pin permissions, cancellation ordering and worker-loop
teardown remain unchanged.
_change_pin_state calls before_write() and then awaits
send_state_event under its own 10 second timeout. When that timeout
expired, or the connection dropped after the request was sent, the
exception branch returned "Matrix pin update failed: TimeoutError" or
the exception's class name. The homeserver may already have applied
the change. The tool reports an unknown outcome only when its own
30 second deadline or an interrupt ends the owning task, and a slow
homeserver reaches the inner 10 second timeout first, so the model
could tell the user that the pin failed although it exists.

_change_pin_state now records that it dispatched the write. An
exception after that point without a Matrix errcode returns the same
unknown-outcome result as the tool, with the step to read the pins
before retrying. A Matrix error response is still a definite failure,
and M_FORBIDDEN is still reported as a rejection.

The new test fails a write with a timeout and with a transport error
after before_write ran. Both returned the plain failure before this
change.
A room without a power-levels event gives the creator level 100 and
everyone else 0, but from room version 11 the create content no longer
names the creator. When a server ignores format=event and returns only
that content, the requester's level is unknown. _change_pin_state
refuses the change in that case, but every fixture returned a create
event with a sender, so no test reached the branch. Without the guard
the comparison of None with the required level raises TypeError, and
the change failed with "Matrix pin update failed: TypeError".

The requester test gains an unknown-level row: no power levels and a
content-only version 11 create response. It expects the pin-power
refusal with level None and no write, and it fails when the guard is
removed.
Pin updates read the requester's power before awaiting the current pin
list. If a moderator demotes that requester during the read, the bot
still writes with the earlier permission result. The homeserver checks
the bot's credentials, so the bot's higher power permits the update.
The shared permission reader also fetched encryption and room creation
after power, which could make inspection report an earlier power policy.

Read the pin state before evaluating the write permission, and fetch
power last in the shared permission reader. Pin and unpin now refuse a
requester whose power or required pin level changed during another state
read. Permission inspection derives its message and pin requirements
from the final power response. Existing owner, room and interrupt checks
remain in place. Matrix cannot make the requester check atomic with the
bot's state write, so a change after the final response can still race.

Two parameterized contracts reproduce revocation and raised pin power
during the pin, encryption and create reads, and check current message
requirements in both encrypted and unencrypted rooms. All 16 cases fail
on the original source and pass with the reordered reads.
Four routing tests pass a recording transport to RelayAdapter, whose
transport parameter requires the RelayTransport protocol. The fixture
only implements its inbound setter and outbound send, so the four
constructor calls fail static type checking even though their exercised
routing paths work.

Implement the complete structural protocol in the recording fixture.
Unused operations raise an assertion error so an unexpected call fails
the test. Keep the recorded action, platform and send result, and narrow
the captured dictionaries before the existing routing assertions.

The correction changes the inherited test fixture. It preserves the
existing contributor records and all original routing assertions.
The encrypted-file fixture returned a constant serialized payload, so
assigning its uploaded URL did not appear in later serialization. The
Matrix SDK serializes the URL stored on the encrypted-file object. The
adapter currently overwrites the outgoing URL, which concealed this
fixture difference in the existing upload test.

Read the fixture URL when serialization is called. Add an invariant
that compares the complete payload before and after URL assignment,
with only the URL overwritten. Keep the existing upload and routing
assertions.
Matrix receives ordinary m.reaction events, but reads and catch-up only
showed message events. The bundled m.relations summary cannot provide
annotation reactions.

Read bounded annotation children through the live Matrix client.
Decrypt encrypted reactions to verify their type, but use the outer
cleartext m.relates_to for the target and key even if the decrypted
payload specifies another relation. Discard redacted events and
deduplicate by sender and key. Present the actor and target in read and
catch-up context without starting a turn. Mark clipped keys and report
incomplete reads when keys, API failures or bounded pages prevent a
complete snapshot.

A five-second batch deadline keeps a slow homeserver from delaying a
turn for every earlier message. Completed lookups remain visible when
others time out. A separate Matrix client confirms that a reaction
appears in the next mentioned turn. Focused tests cover duplicate and
redacted reactions, encrypted events, long keys, lookup errors, and
the deadline.

Bind the real room-policy predicate in the read fixtures. Read admission
rechecks room policy after awaiting room identity, so the fixture must
provide the same adapter protocol as the production path.
Source-permalink and reaction-context branches each test their own prompt
contract. Their combined adapter adds the trigger permalink to catch-up
messages, so the reaction fixtures need the SDK's asynchronous state-store
methods and complete expected source notes.

Merge both feature histories without changing their existing commits.
Preserve reaction author trust, room and thread context, transcript roles,
cache privacy and the source-permalink encoding and routing rules. Use the
real MemoryStateStore in the affected fixtures and retrieve SessionSource
from the final context tuple item. Check each complete catch-up prompt
against its explicit room, trigger event and via server, including the
first event of a batched turn.

The four composed gateway files cover 343 cases. The affected two files
pass all 264 cases after the fixture adaptation; the unchanged session and
project-isolation files passed their 79 cases. Scoped Ruff reports no lint
diagnostics. Existing formatter differences remain qualified separately.
Live Docker room and thread backfill validation is still outstanding.

This dependent layer preserves both parent histories. Its two live
backfill fixture adaptations do not claim complete original 858 delivery.

Historical composition qualification

The preceding message records the original two-parent composition.
This successor reapplies its fixture integration after the individually
preserved prerequisite, SDK and reaction contributions on captured main
648a309. Its history has one parent.
The original composition message remains complete for provenance; this
commit does not create a merge or establish live Matrix acceptance.
The Matrix SDK retries rate-limit responses and connection timeouts
indefinitely by default. A failed request during account or room setup
can therefore stop the whole native module until the outer runner kills
it, without reporting the setup error.

Disable those retries and bound ordinary fixture requests to fifteen
seconds. Apply the same configuration to registration,
restored accounts and the encrypted observer. The fixtures now expose
the SDK error so the failing operation can be diagnosed directly.

A real loopback HTTP server reproduces the repeated rate-limit requests
through registration and restored accounts. Both paths now make one
request and return the expected error.
The Matrix SDK retries rate-limit responses and connection timeouts
indefinitely by default. A failed request during account or room setup
can therefore stop the whole native module until the outer runner kills
it, without reporting the setup error.

Disable those retries and bound ordinary fixture requests to fifteen
seconds. Apply the same configuration to registration,
restored accounts and the encrypted observer. The fixtures now expose
the SDK error so the failing operation can be diagnosed directly.

A real loopback HTTP server reproduces the repeated rate-limit requests
through registration and restored accounts. Both paths now make one
request and return the expected error.
The original feature tests predate the canonical approval lifecycle,
permalink source fields, room policy owner argument and SDK send error
classification. Their doubles omitted asynchronous state-store reads,
exact approval request IDs and terminal card edits. Several complete
expected events also omitted the newly composed source and mention
metadata, so the full feature gate failed before checking its intended
behaviour.

Update those fixture bindings and complete expected values for the
current defining owners. Keep every original parameter row, delivery
attempt, payload, cancellation and authorization assertion. Thread
reconnect expectations retain confirmed delivery and cache identity.
Catch-up expectations include the permalink note before the new message.

The complete nine-file feature boundary passes all 808 cases with the
actual installed Matrix SDK, four workers and no file retries. The full
Matrix file finishes in 30.7 seconds, including the event-loop lookup
contract that timed out on the earlier incomplete source composition.

Original858 public source qualification

The preceding 808-case result records the original composition. It does
not describe a new run against this public tree. The published backfill
composition has 343 scoped cases, using 264 final affected cases and 79
unchanged earlier successes. Its inherited formatter and type findings
remain qualified, and live Docker validation remains pending.

The other feature contracts are represented by their existing public
owners. Their fixture expectations follow the actual approval, notice,
location, send-classification, read-policy and delivered-event APIs.
Expectations from the original later composition do not establish bugs
in standalone owners that use an earlier API. No combined six-module
endpoint or repeated native gate is required for this source credit.

This source-neutral contribution preserves the complete original
message and both original contributor dates. It records the distributed
source qualification without changing the published source tree.

Current linear successor qualification

The 343-case result above describes the earlier a76019d composition.
This contribution follows the verified single-parent successor a83881c
and keeps its source tree unchanged. The accepted seven-file gateway
gate for that successor passes 362 cases. Live Matrix validation,
private integration and whole-campaign completion remain separate.

The feature-contract source is already supplied by the published
Matrix context prerequisite. Keep this original empty contribution
as an explicit credit record while retaining the current supplier
fixture maintenance.
@iainlane
iainlane force-pushed the feat/matrix-pins branch 2 times, most recently from 0fa7531 to ef7c492 Compare October 8, 2026 08:20
The fake HTTP API discarded the client_session supplied by the Matrix
adapter, so disconnect closed a mock while the real aiohttp session was
left open. A reconnect test also replaced disconnect with a stub and
left its new connection open after checking the guard. Strict resource
warnings expose both leaks.

Preserve the supplied session in the fake API. Use a wrapping disconnect
spy in the reconnect test and disconnect the replacement client after
the existing call assertion. The fake API and reconnect fixture predate
this branch, so this focused fixture maintenance is separate from the
corrections folded into their campaign contributors. Production code
and all original assertions are unchanged.

The prebuilt Client fixtures also use the already published factory from
reaction-watch commit a58c137. That factory assigns the supplied HTTP API
to the prebuilt client, so disconnect closes the same real session that
the adapter created. This branch predates those fixture bindings. Only
the factory and its fourteen existing constructor assignments are
propagated; all production code and assertions remain unchanged.
Matrix edits and redactions can arrive while context is being fetched or
while media is being prepared. Cached reply text could therefore disagree
with the event state that was current when the model received the turn.
Encrypted replacements also needed validation before changing the text.

Track effective originals and replacements, retain event dependencies
across asynchronous boundaries, and refresh admitted room and thread
history before model input. Use the shared history decryption helper and
validate encrypted edit ownership before applying a replacement. Platform
hooks retain the references needed to render the final input on the event
loop, including quoted attachments and unavailable-parent markers.

Include the feature's API migrations and their matching unit and native
fixtures in this commit so the baseline uses the current prerequisite
interfaces. The remaining boundary fixes and test refinements stay in
separate commits.

Bind the real room-policy predicate in the read fixtures. Read admission
rechecks room policy after awaiting room identity, so the fixture must
provide the same adapter protocol as the production path.

Preserve the required upstream builtin typing and unused-import cleanup
while adding the original input revalidation callbacks. Media merging
uses the contribution event method, which pads the aligned inline flags.

The new gateway test callers create real session stores. Register their
database cleanup with a scoped ExitStack so the complete test case closes
every owned handle after its assertions, including replaced stores.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

(cherry picked from commit 25f9ee3)

The pin fixtures dispatch JSON text and registered tool entries, but
their declarations also allow mappings and absent entries. Add the
already-published fixture preconditions before decoding text, calling
the entry handler or extending a mapping expectation. Original result
assertions and all later profile and routing fixtures remain unchanged.

Public-corrected-source: 5df5d6f
Public-corrected-head: 56b8ddb

The synchronous refusal case can create the process tool loop even
though it rejects a CLI caller. Apply the published NousResearch#125882 fixture
cleanup to close only the loop that this case created. Preserve a loop
that existed before the case and all original toolset and error
assertions.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

(cherry picked from commit 25f9ee3)

The pin fixtures dispatch JSON text and registered tool entries, but
their declarations also allow mappings and absent entries. Add the
already-published fixture preconditions before decoding text, calling
the entry handler or extending a mapping expectation. Original result
assertions and all later profile and routing fixtures remain unchanged.

Public-corrected-source: 5df5d6f
Public-corrected-head: 56b8ddb

The synchronous refusal case can create the process tool loop even
though it rejects a CLI caller. Apply the published NousResearch#125882 fixture
cleanup to close only the loop that this case created. Preserve a loop
that existed before the case and all original toolset and error
assertions.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

(cherry picked from commit 25f9ee3)

The pin fixtures dispatch JSON text and registered tool entries, but
their declarations also allow mappings and absent entries. Add the
already-published fixture preconditions before decoding text, calling
the entry handler or extending a mapping expectation. Original result
assertions and all later profile and routing fixtures remain unchanged.

Public-corrected-source: 5df5d6f
Public-corrected-head: 56b8ddb

The synchronous refusal case can create the process tool loop even
though it rejects a CLI caller. Apply the published NousResearch#125882 fixture
cleanup to close only the loop that this case created. Preserve a loop
that existed before the case and all original toolset and error
assertions.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

Upstream compatibility (2026-10-09): keep the current Spotify catalog
plugin move while adding the Matrix administration defaults and pin table.
The removed core Spotify documentation is not restored; the complete
Matrix pin permission and session contracts remain unchanged.

Apply the published NousResearch#125882 fixture lifetime corrections to this
pin contribution. Close executor worker loops after dispatch finishes
and close the main tool loop only when the test created it. Preserve
the existing profile routing, permission fixtures, cancellation
ordering and every behavioural assertion.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

Upstream compatibility (2026-10-09): keep the current Spotify catalog
plugin move while adding the Matrix administration defaults and pin table.
The removed core Spotify documentation is not restored; the complete
Matrix pin permission and session contracts remain unchanged.

Apply the published NousResearch#125882 fixture lifetime corrections to this
pin contribution. Close executor worker loops after dispatch finishes
and close the main tool loop only when the test created it. Preserve
the existing profile routing, permission fixtures, cancellation
ordering and every behavioural assertion.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 11, 2026
The agent can inspect pinned messages but cannot change the pin list.
Add pin and unpin actions through the adapter for the current Matrix
session, with the existing room and requester policy.

Put the action in a separate Matrix administration toolset that users
must enable. Preserve the order of existing pins, serialize concurrent
updates from this adapter, and return the homeserver permission
rejection when the bot cannot write room state.

A separate matrix-nio client observed a rejected update followed by
successful pin and unpin operations that preserved an existing pin.
Focused registry and state tests also cover session scope and concurrent
updates.

Read and update pins through the room inspection context supplied by
this feature's prerequisite. The context applies admission and current
permission checks, preserves existing state content and controls the
interruptible worker. Keep a write outcome unknown once cancellation
or a transport failure prevents confirmation after the request was sent.

Include the interface and cancellation migrations with the initial
feature so this commit uses the current inspection and scheduling APIs.

(cherry picked from commit 25f9ee3)

The pin fixtures dispatch JSON text and registered tool entries, but
their declarations also allow mappings and absent entries. Add the
already-published fixture preconditions before decoding text, calling
the entry handler or extending a mapping expectation. Original result
assertions and all later profile and routing fixtures remain unchanged.

Public-corrected-source: 5df5d6f
Public-corrected-head: 56b8ddb

The synchronous refusal case can create the process tool loop even
though it rejects a CLI caller. Apply the published NousResearch#125882 fixture
cleanup to close only the loop that this case created. Preserve a loop
that existed before the case and all original toolset and error
assertions.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/matrix Matrix adapter (E2EE) sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants