Repository navigation
Conversation
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.
iainlane
force-pushed
the
feat/matrix-pins
branch
from
October 1, 2026 09:36
7aa8084 to
5031a7c
Compare
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.
iainlane
force-pushed
the
feat/matrix-pins
branch
from
October 2, 2026 19:07
d86594f to
56b8ddb
Compare
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
force-pushed
the
feat/matrix-pins
branch
2 times, most recently
from
October 8, 2026 08:20
0fa7531 to
ef7c492
Compare
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
force-pushed
the
feat/matrix-pins
branch
from
October 9, 2026 12:02
ef7c492 to
8da3b75
Compare
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
force-pushed
the
feat/matrix-pins
branch
from
October 9, 2026 21:49
8da3b75 to
7c31c52
Compare
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
force-pushed
the
feat/matrix-pins
branch
from
October 10, 2026 00:39
7c31c52 to
aaee180
Compare
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_pinwith explicitpinandunpinactions for the current Matrix room. It changes the room'sm.room.pinned_eventsstate through the adapter and client that received the current message.The tool is in a new
matrix_admintoolset, which is restricted to Matrix and disabled by default. Enable it for Matrix inhermes 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:
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 astate_defaultof 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.M_FORBIDDENwith 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
unchangedwithout a write. Redacted pin state ({}) and a nullpinnedvalue 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. Apinnedvalue 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=pinsbefore 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_adminandmatrix_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.