Skip to content

feat(matrix): expose unread counts and scoped mark-read actions - #126398

Draft
iainlane wants to merge 478 commits into
NousResearch:mainfrom
iainlane:feat/matrix-unread
Draft

iainlane wants to merge 478 commits into
NousResearch:mainfrom
iainlane:feat/matrix-unread

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.

Matrix sessions can inspect history but cannot read the bot account's observed notification counts or deliberately acknowledge a selected event and timeline. Add matrix_unread and matrix_mark_read through the Matrix session toolset, with a hermes tools toggle. Counts describe the bot account's push rules and remain unavailable before server observation.

Explicit acknowledgements require the exact event, scope and public/private visibility. Each write checks current requester membership, room policy, requester/target-sender authorization and the original live owner. Tool interruption cancels and drains the owning task while its gateway loop is running. If the loop stops, the tool returns without claiming a completed drain and prevents a cancelled preflight from issuing a new write after recovery. Confirmed receipts and uncertain receipt/account-data writes remain separate in the result. Only room scope resets the bot account's marked-unread flag; the fully-read marker remains unchanged.

Current prerequisite composition and review

This successor retains the actual independently published receipt supplier dc2811135e5f3a3bdc670ad69b8973c412b33806. Review the own unread layer above that exact supplier. The complete 28 current own contributions remain in their original order, with literal author and committer metadata, dates, timezones and full messages. All thirteen meaningful empty own records remain. Every complete cumulative snapshot joins automatically onto the supplier, without another production implementation or conflict resolution.

The compatibility target is captured upstream 3637c512fc3211bdccb61b7140d09ac1018508ba. Local joins with that revision and the original frozen target are clean. The current receipt supplier already includes the generic worker-loop owner and finaliser. Its four complete defining scopes are AST-exact to the independently published core correction. Both complete pin and read files pass with this production, so the earlier explicit worker fixture ports are unnecessary here. The complete request-loop result mechanism also matches the separately published #130706 source, with formatting as the sole difference. Its logical prerequisite remains.

Current validation and limits

All 199 cases across seven complete SDK files pass with zero resource warnings. The 31-case pin and 17-case read files passed first on the final unchanged source; the disjoint five-file unread, sync, warning, invite and runtime scope then passed 151 cases on that same source. Each run used one worker, zero retries and all four strict warning categories. Both runs captured all 8,553 tracked Python and test-runner inputs before and after execution. The canonical runner was restored exactly.

A fresh PM-managed image of this exact source passes both original plain and encrypted native unread cases. Separate Matrix clients observe notification counts, the selected room and thread receipt scopes, public/private acknowledgements and account-data updates. The complete native child is clean through process exit, with strict warnings and no retries. The image, package and lock inputs and actual tracked native runtime bindings are recorded separately. The original native assertions and parameters remain unchanged.

The complete 17-path type assessment reports 51 inherited findings, compared with 51 on the original current branch and 59 on the current supplier. Every complete lexical, statement-AST and relative statement-span occurrence is within the original/provider maximum, with zero introduced findings. Ruff passes. Thirteen formatter paths remain inherited; this successor adds none. Cumulative whitespace checks pass.

These are exact local SDK and live Matrix receipts. Earlier broad and hosted results below remain historical, and this delivery does not claim fresh hosted CI acceptance.

Previous recorded delivery

The following counts, commit references and runtime evidence describe
those earlier recorded sources, not the current composition.

Previous recorded prerequisite composition

Merge receipt lifecycle, Matrix administration and the shared runtime lifecycle repair first. This branch composes the complete published receipt parent with the corrected inspection, pins and administration contributions. The twelve prerequisite contributions retain their original authors, dates and complete messages; runtime-equivalent contributions retain explicit credited commits. The fourteen original unread contributions are preserved above a separate mechanical extraction of the existing sync topic.

This PR targets upstream main, so the cumulative diff includes its prerequisites. The own review comparison is the unread composition above published receipt parent 6bbef00ea3501a0ba7e56de7bf24ad43648f523e. The rebase uses fixed upstream 9a71d5a8b59b37d1795ab7065fbbe3b647ba5249 because GitHub confirmed an upstream conflict. A later change to main alone does not trigger another rebase.

Current live authorization, transport identity, profile and physical-home checks from the receipt parent remain intact. The shared stopped-loop runtime and its complete separate-loop test module are byte-identical to the corrected published #130706 contribution. The original stopped-loop source and the published unread copy have identical author, date, full message and patch. Their explicit source aliases use the unread contribution as one canonical credit; the redundant final empty copy is omitted in this clean history. All runtime and native inputs remain exact; the inherited receipt-parent unit fixture is the only tree difference.

Previous recorded validation

The actual Mautrix initial-sync contract reproduced missing unread filters for both a fresh sync and a saved cursor. Both rows failed before the correction and pass after the original unread feature supplies the filter to every sync branch. The complete unread unit module passes 64 cases. Existing decorators, parameter tables and whole assertions remain intact; three callback fixtures now use actual AsyncMock instances, as required by the preserved contributor typing correction.

The complete affected type comparison checks 146 Python paths, including every original affected unit module, with the actual SDK sources. The corrected composition has 1,336 diagnostics against 1,344 on exact published receipt parent 6bbef00ea3501a0ba7e56de7bf24ad43648f523e, with zero introduced diagnostic buckets. The comparison preserves complete rules, messages, qualified scopes and multiplicity. Only the four independently proved sync method moves receive explicit scope mappings. The previously corrected extraction annotations remain unchanged.

Signed candidate 4c04f0ae9aba6ef067a2d17537787636bf499e73 passes all 910 tests across 16 files in 74.8 seconds, with four workers, strict warning errors and zero failures, skips or retries. The original 15-file scope passes 892 cases; the inherited receipt-parent unit fixture adds 18. Committed-delta whitespace and Ruff checks pass for all 18 selected Python files. All 407 cumulative contributions are signed and linear. All 180 sources from the previously published unread branch retain their exact raw author, date, timezone and complete message prefixes. The clean history contains 27 own contributions and omits only the redundant final empty runtime credit. Original c3/DDDE runtime aliases resolve to the single preserved unread contribution; every runtime and native byte remains unchanged.

The complete admin test module is byte-identical to the published receipt parent and the protected accepted unread tree. Its complete decorated catalogue function equals the corrected published #126437 owner. The fixture explicitly disables unrelated optional toolsets and preserves the original expected matrix_read, all parameter rows and the A/B/A profile contract. The correction already exists in the published parent, so the unread replay adds no second fixture correction.

Both original native unread cases passed on protected head a99a6567e7a8d46e566f1cd875076a7557d00489 in 52.1 seconds, with one worker, no retries and the unchanged 300-second budget. Those results transfer to this candidate through independently verified identical inputs: all 6,746 image source files, 2,441 required runtime files and 39 mounted native harness files match the accepted image. The original complete native module and every production blob and mode are unchanged. The sole tree difference is a Docker-excluded unit fixture inherited from the receipt parent. Git's existing PowerShell line-ending conversion is explicit in the input proof. No new image build or native execution is claimed for this candidate. The accepted image is sha256:d7c0a1107d3be7628b3751808066766d70ff95729d280ce853fbb0de23539cbb; its SDK versions are Mautrix 0.21.1, matrix-nio 0.26.0 and aiohttp 3.14.3.

The earlier protected composition passed 892 unit cases after the public admin correction disabled unrelated toolsets. The separate original catalogue owner is now published at b9fa14ac69185bab4e0bcab904c24c22381ff0a7; the receipt parent contains that exact corrected catalogue contract. These are public source-owner corrections. The first setup attempt for this clean candidate was blocked before tests started; the complete passing runner receipt above describes the frozen candidate.

Historical validation

The following receipts describe the earlier published heads and remain historical. They do not establish acceptance of the current rebased composition.

The cancellation and current-membership contracts reproduce actual interruption and policy defects before repair. Session, home and profile replacement inside fake callbacks are controls for the shared runtime; those controls do not establish a separate production race.

Before the read-only follow-up, signed head 36fad5c7d960a659b52bbe7088a9c63019160fb9 passed 1,394 tests across 34 files, with four workers and zero retries or skips. A fresh image of the same head passes both plain and encrypted native cases in 42.7 seconds. Independent Matrix clients observe the selected room and thread acknowledgement scope, counts and account-data changes. The same exact image passes all 13 existing receipt visibility and processing-boundary cases in 203.0 seconds, including manual plaintext approvals and the specialized model responders. Ruff, the affected production-module type checks and whitespace checks pass.

The composed receipt fixtures use observed joined members for DM controls and preserve queued fake-model tool calls. Their earlier failures are recorded in the review report and pass after these fixture changes. The encrypted local history index remains a separate follow-up.

Read-only unread requests now observe agent interruption and the shared thirty-second deadline. Running-loop cancellation stops the owning task before the worker returns. Reads never signal a write and therefore return an ordinary interruption or timeout error. An event-controlled registry contract reproduces both blocked-read failures on the previous head and preserves the accepted-read control.

Signed head 31ba23a5831034576d0e7181202efac1512789b0 passes the three-row read contract and all 414 tests across nine distinct adjacent files, with four workers and zero retries or skips. Whole-tree Ruff, the edited tool's type check with the actual Mautrix source and whitespace checks pass. A fresh image of this exact head passes both plain and encrypted native unread cases in 43.1 seconds. The 13 receipt native cases above are recorded evidence from the previous head, which this read-only change does not claim to rerun.

The final runtime composition passes 326 tests across five existing files, with four workers and no retries or skips. Its separate real-loop contract verifies stopped admission, preflight loss, uncertain dispatched writes, confirmed results and recovery without a late write. A fresh image of this exact composed head passes both plain and encrypted native unread cases, 2/2 in 44.2 seconds, with no retries or skips. Earlier native receipts above remain historical.

The fresh final image digest is sha256:6e9fa0c6623b7b9d1cce7e5b233209932fb74a2a4c110fa7801139214f10dead. Hosted CI remains subject to upstream approval.

Original fixture credit

The complete original shared-fixture correction is recorded in commit 8305c5aebde8e34bab7af12d56943eef57d3a4f2, with both original contributor dates and the full original message. Its parent is 4c04f0ae9aba6ef067a2d17537787636bf499e73, and both commits have source tree 552b35f28c62dc090c7a3b859a8a90b307673e58. The branch now includes the unchanged 407 signed contributions and this signed empty credit contribution.

The complete fixture effect was already present through the shared configuration and scoped feedback settings. This credit changes no source and adds no fresh runtime or native acceptance. The validation results and native transfer qualifications above remain historical evidence for the unchanged source. The reported conflict with current main remains; this credit does not establish main compatibility.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets platform/matrix Matrix adapter (E2EE) area/config Config system, migrations, profiles area/docker needs-decision Awaiting maintainer decision before any implementation 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-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 28, 2026
chelsealong and others added 15 commits October 1, 2026 10:16
matrix.process_edits was documented and tested as working via config.yaml,
but _YAML_LOWER_KEYS (the apply_yaml_config_fn hook that turns config.yaml's
matrix: block into env vars) never listed it, so setting
matrix.process_edits: true in config.yaml silently did nothing — only the
MATRIX_PROCESS_EDITS env var actually worked. Add the missing tuple entry
(mirroring process_notices) and a test that drives the real
_apply_yaml_config bridge instead of only asserting against a
hand-constructed PlatformConfig(extra=...).
The initial edit option used a global boolean and trusted the relation
inside m.new_content. An edit could therefore change another author's
input, route a correction into a different thread, or enter the active
turn through the gateway's interrupt or steering policy.

Configure corrections with exact room IDs and boolean values, defaulting
to no enabled rooms. Validate the original event's room and author with
the receiving adapter's client, then use its original thread relation.
Apply the current sender, room, notice and mention policies. Typed edits
must supply genuine replacement content because mautrix serialization
can manufacture m.new_content. Encrypted validation uses the existing
client crypto and replacement reader.

Queue corrections through the existing pending slot and FIFO, replacing
pending edits to the same original in their existing position. Recheck
both events and current authorisation in shared turn preparation, so
cold and recursive queued turns reject withdrawn corrections even after
cache eviction. Edited commands remain external conversational text.

Append the correction as a new user turn with its original event ID.
Earlier model messages and the system prompt remain unchanged. Keep the
existing opening-event reply anchor for final delivery in a queued chain.

Two parameterised invariants reproduce the original policy, routing and
active-turn failures. Six additional rows reproduce a withdrawn pending
replacement after cache eviction. All 50 rows pass with the repair, and
the adjacent gateway gate passes 766 tests across 39 files.
With busy_input_mode: queue, the base adapter debounces ordinary text
that arrives during a turn and merges the buffer into the pending slot
when the slot holds an event from the same sender. A Matrix correction
waiting in that slot is a text event from the same sender, so the
user's next message was appended to it. At turn start the adapter
compares the queued correction with the server's copy of the edit. The
merged text no longer matched, so the gateway dropped both the
correction and the new message without a reply.

Debounced text is now never merged with an event that waits for a turn
boundary. The text stays in the debounce buffer while the correction
occupies the slot, and the existing flush at the end of each turn moves
it into the slot as its own turn. When a correction arrives while text
is already buffered, the adapter flushes the buffer first, so that text
runs before the correction, in the order the user sent them.
Edit handling and turn-start validation resolved a correction's
context with the original event ID. _resolve_message_context records
that ID as the thread's latest event, which bot messages without an
explicit reply target use for their reply fallback, and it sends a read
receipt for it. Each correction therefore moved the thread's fallback
target back to the original and sent a receipt for an older event. The
edit handler did this before it checked the sender's authorisation.

_resolve_message_context now accepts record=False, which leaves the
thread trackers and the read receipt to the caller. The correction
handler resolves the context this way and checks authorisation first.
Only an accepted correction marks its thread and sends a read receipt,
and the receipt is for the edit event. Turn-start validation records
nothing.
A correction event's message ID is the ID of the m.replace event.
Clients display a replacement as the original message and do not show
the edit event itself, so the processing, success and failure
reactions that the adapter placed on the edit were invisible.
Corrections now use the original event as the reaction target.
The bridge test set MATRIX_PROCESS_EDITS before applying the YAML
configuration and then asserted that the value was unchanged.
apply_yaml_bridge never overwrites a variable that is already set, so
the test would also pass if a process_edits bridge row came back. The
test now starts with the variable unset and asserts that it stays
unset. With a bridge row restored, it fails.
The edit follow-up section did not say which message a correction's
reply refers to. A correction's reply is a Matrix reply to the original
message when the correction starts its own turn or runs before the end
of a queued chain. When the correction is the last queued message to
run after a busy turn, the gateway sends the reply as a Matrix reply to
the message that started that turn, as it does for every queued
message. Document this, and document that corrections are never merged
with other messages, that the model receives the corrected text as
typed, and that processing reactions appear on the original message.
The effective-state parent added requires_mention as the sixth field of
the tuple from _resolve_message_context. The correction handler still
unpacked six fields, so every accepted edit raised ValueError before an
event was built, and no correction reached the gateway.

Unpack the new field as well. The handler does not use requires_mention
itself: _build_inbound_event reads it from the same context and records
matrix_requires_mention on the correction as it does for other messages.
The effective-state parent removed fetch_mention_context and
take_turn_channel_context from the adapter, and the gateway now calls
use_turn_context on the snapshot from fetch_inbound_context. The
correction tests stubbed all three methods, so monkeypatch failed on the
missing attributes, and a None snapshot would fail in the core.

Drop the stubs so turn preparation uses the adapter's real snapshot and
prepare_turn_context. The hook looks up the session through the runner's
session store, so the cold runners get a SessionStore in tmp_path, and
the room identity fake has room_state instead of the removed
members_digest.
The effective-state parent moved the Matrix client helpers into
client_events.py, which provides Method with its own stand-in for
environments without mautrix. The correction module still had a private
try/except that fell back to read_context's re-export. Import Method
from client_events, as the other Matrix context modules do, so the
plugin has one fallback definition.
A correction must wait for a turn boundary and take its own turn. The
busy-session handler already queues a turn-boundary event through the
FIFO. The runner has a second route to a running session:
_hm_handle_running_session_message, reached from _handle_message when
the runner has a running turn for the session but the adapter has no
session guard. That route ignored _queue_at_turn_boundary.

The state is reachable in production. The CLI handoff runs its
synthetic turn through _handle_message directly, so the adapter has no
guard for that session for the whole turn. Auto-resume after a restart
claims the pending-agent sentinel before its task reaches
handle_message, and expects inbound messages in that window to reach
this route. A correction arriving then was merged into the pending text
while the agent was starting, and in interrupt or steer mode it was
delivered into the running turn.

Skip the pending-agent merge for turn-boundary events and send them
through the queue-mode branch, which uses the same FIFO as the busy
handler. The drain check still comes first, as in the busy handler.

The new test builds a correction with the real Matrix adapter and passes
it to the real runner while the session has a running turn and no
adapter guard. Before the change it failed in three of four modes: the
pending-agent case produced "earlier text\nlatest correction", interrupt
mode called interrupt("latest correction"), and steer mode steered the
correction into the turn. Queue mode already queued it.
The effective-state parent now contains NousResearch#125648, which quotes the
triggering event once per threaded response. A send anchored on an
event that Hermes has already answered in the thread is a
continuation: it sets is_falling_back and points at the latest event
in the thread.

A correction that waits behind a paused turn runs as the last message
of the queued chain. Its answer is anchored on the message that started
the paused turn, as for every queued message, and the first response
already answered that message. The native test still expected both
responses to be genuine replies to that message, so the plain and
encrypted enabled cases failed with is_falling_back true and
m.in_reply_to pointing at the first response.

The client now expects the first response to quote the blocked message
and each later response to fall back to the response before it, in the
expected order. The Matrix guide describes the correction's reply in
that case as a thread message after the running turn's answer.
…b it

In busy_text_mode queue, text that arrives during a turn waits in a
per-session debounce buffer. _flush_text_debounce_now moves the buffer
into the adapter's pending slot, but only when the slot is empty or its
event may absorb the text. Two senders' texts may not merge, and since
the Matrix edit follow-ups a queued correction absorbs no text at all.
When the slot could not absorb the buffer, the flush returned without
moving it and cancelled its timer. The buffer then waited for the next
flush.

_queue_text_debounce made that loss visible. A message from a
different sender first flushes the buffer. When the flush failed and
the slot could not absorb the new message either, the function returned
without storing it, and nothing logged the drop. Threads are shared
between their participants by default, so in queue mode a correction
followed by texts from two people lost the second text. On main the
same happened to a third sender's text while the slot contained text from
someone else. The unflushed buffer also broke arrival order: text sent
between two corrections ran after the second one, because the second
correction went to the runner's FIFO while the text stayed in the
buffer.

When the slot cannot absorb the buffer, hand the buffered event to the
runner's _queue_or_replace_pending_event, which appends it to the FIFO
behind the slot with the same cap and album rules as other busy
follow-ups. The adapter already reaches the runner through
gateway_runner for delivery scope and live-adapter lookups. The buffer
is then empty, so the next sender starts a fresh buffer instead of
reaching the drop. Each sender's burst still runs as one turn, in
arrival order. An adapter without a runner keeps the previous
behaviour, because it has no FIFO.

The platform-neutral test sends text from two senders while the slot
contains a third sender's text, and while it is empty. Before the change,
"three" was missing in both rows and a buffer stayed stranded with no
timer. The Matrix test covers a pending correction followed by text
from Alice and Bob, and from Bob and Carol, with and without the timer
firing between them. Before the change the second message was lost in
all four rows.
Text that the user sends after one correction and before another must
run between them. Before the previous commit, the text stayed in the
debounce buffer because the first correction in the pending slot could
not absorb it, while the second correction went to the runner's FIFO.
The observed order was $edit1, $edit2, $text.

The debounce flush now hands the text to the FIFO behind the first
correction, and the flush before the second correction's busy handling
runs first, so the order is $edit1, $text, $edit2. This test asserts
that order. It failed on the parent of the previous commit with
[$edit1, $edit2, $text].
A correction's m.new_content has no reply fallback, so a leading quote
in it is the user's own text. After the merge of the effective-state
parent, three conditions in the adapter strip a quote only when
reply_fallback is true and _has_reply_fallback recognises a legacy
fallback. The existing quote-on-reply row starts with "> quoted line",
which _has_reply_fallback rejects because it has no sender pill, so
that row passed even with reply_fallback removed from the conditions.

Add rows whose body quotes Bob with a pill and a blank line, which is
the shape of a legacy fallback, for an original that is a reply and for
one in a thread. With "reply_fallback and" removed from the three
conditions, both rows fail with "my answer" in place of the full quoted
body. The two older rows pass under that mutation.
A started input can be deferred into the FIFO overflow and rescued by a
later turn. After the rescued turn finishes, the runner clears its durable
active-turn marker before reporting processing completion. Cancellation
during that storage await skips the completion hook, so the input remains
marked as processing even though its turn has finished.

Run rescued-input completion in the marker cleanup finally block. Keep
both awaits after the generation-scoped session and lease releases, and
retain the adapter-owned marker handoff and marker token comparison.

Extend the marker cancellation contract with the actual adapter start and
defer lifecycle followed by FIFO rescue. Both normal and rescued turns
release ownership and admit a successor; the successful rescued input
also reports exactly one successful completion after cleanup cancellation.

Historical attribution for original source 51c0ecf6197ce1489fb31bdcbddfb64c3d2ba125
(audit record 755). Public PR NousResearch#126283 already includes this cancellation
correction in 48902ee. This empty commit
records the missing original contribution credit. The source tree is
unchanged.
Matrix message dispatch and context admission remain in the oversized
adapter facade. Opt-in correction handling needs both entry paths.
Move their existing methods into MatrixInboundEventMixin before adding
that behaviour, preserving every dispatch, mention, thread, timestamp
and receipt decision.

Resolve facade-bound helpers lazily so existing runtime SDK bindings and
patch targets remain effective. Declare the actual MatrixAdapter host
for the constructed and admitted events. Update the module guide to
include its expanded topic.
Speech reference tests used empty history, so they did not exercise
channel-state acknowledgement while references appeared in stored
transcript rows and adapter-generated context.

Extend the existing A/B/A contract with empty and stored history for all
five speech variants. Check the complete prepared message, unchanged
transcript, lookup and adapter calls, returned channel state and single
raw authored preprocessing input. Stored references and generated channel
notes remain literal while caption and successful speech references
expand under the owning profile.

The complete file passes 21 cases with strict asynchronous warnings and
zero retries. This adds coverage to the existing preparation behaviour;
it does not repair a reproduced production defect.
Preparation can derive a user-input receipt from the transport message
before shutdown assigns a pending UID. Reconstruction then derives a
second receipt from that UID, so an already persisted user turn appears
unexecuted. Input without a transport ID also minted a new random receipt
on each call.

Cache a typed owner with the actual transport namespace and bind the saved
owner to the pending UID at capture. Reconstruct that binding only after
checking its profile, workspace, conversation, pending UID and derived
receipt. Event aliases and selective withdrawal retain the aggregate
owner even when the first contribution is removed. Existing transport-ID
receipts keep their original derivation.

The real A/B/A SessionStore contract reproduces the mismatch at the
prepare-before-capture, withdrawal and alias boundaries. Capture before
preparation remains a control. Scope and receipt changes in a saved record
are refused. This records an execution identity; RAM admission still does
not consume a pending recovery record.
Speech reference tests used empty history, so they did not exercise
channel-state acknowledgement while references appeared in stored
transcript rows and adapter-generated context.

Extend the existing A/B/A contract with empty and stored history for all
five speech variants. Check the complete prepared message, unchanged
transcript, lookup and adapter calls, returned channel state and single
raw authored preprocessing input. Stored references and generated channel
notes remain literal while caption and successful speech references
expand under the owning profile.

The complete file passes 21 cases with strict asynchronous warnings and
zero retries. This adds coverage to the existing preparation behaviour;
it does not repair a reproduced production defect.
Discord event normalization and Slack's inbound guard still occupy their
large adapter facades. Move those methods into their platform siblings,
with deferred facade bindings for runtime SDK and helper resolution.

Extract the pending withdrawal traversal behind its existing sender
predicate so native deletion notifications can reuse every pending store.
The traversal and existing deletion policy remain unchanged.

Preserve every complete current Discord notification and Slack intake guard in its extracted owner, including current subtype admission and workspace checks. Retain all current Discord mixins and the required deferred SDK and helper bindings.

Keep the optional changed-message result separate until the existing None guard, then assign the narrowed event.
Cached text can contain an opaque marker. Exercise
normal and restored queue execution with the actual model request path
so the test detects transcript substitution, role changes and profile
owner leaks before the request reaches the model.
An accepted leftover /steer can arrive while provisional pending input
has already filled the queue. The drain ignored FIFO admission failure,
so it dropped the correction while keeping the provisional input. The
branch that prioritised a steer over a queued event also restored that
event directly without releasing its provisional dispatch reservation.

Retain an unadmitted steer for the next turn. Check each pending input
through the recognised-command guard before combining the remaining
text, so a residual command is discarded without losing the correction.
Restore deferred FIFO events through the existing dispatch helper, which
releases reservations while preserving order.

Three assertion failures reproduce the lost correction with ordinary
provisional text, the residual /help safety case, and the restored FIFO
state on the parent. The FIFO contract releases each returned dispatch
before measuring queue depth, as the caller does in normal operation.
All eight contracts in the existing file pass after the fix.

Integrate the pending-admission correction with the current pinned channel-input constructor. The drain now uses the actual FIFO admission result, preserves unadmitted steering text and releases a deferred event reservation through the existing restore helper. The complete original regression contracts and assertions remain unchanged.
A steer accepted by the running agent can return unconsumed while earlier
queued input and provisional dispatches fill the pending queue. Admission
then fails when the drain moves the steer to its own turn. Dropping that
accepted input loses the correction and its processing completion.

Restore the tracked steer through the existing pending-dispatch helper and
park its lifecycle before transferring completion ownership. Restoration
preserves accepted input, while the admission threshold still rejects new
messages. The earlier queued event keeps its own dispatch and runs first;
the restored steer retains its message identity and completes with its own
turn or with cancellation if the queue is stopped.

The regression drives the real runner and adapter with a scripted model,
queued input and a full set of provisional reservations. It checks model
input, admission rejection, processing outcomes and retained reservations
for successful delivery and /stop.
The receipt teardown contract observed the former text-only flush call.
The current adapter writes a complete pending snapshot before clearing
its slots, so that observer missed a successful write. Wrap the actual
snapshot writer and retain the completion, ordering and empty-slot
assertions. The wrapper still writes the real snapshot.

Supply joined room members for the current Matrix authorization check.
Combine the native feedback settings with the existing paused-queue
configuration in one display section, and retain the additive auxiliary
configuration fixture required by the unread tool. These fixtures use
the current adapter and configuration paths without replacing their
behaviour assertions.

The fixture contribution is split between the receipt and unread layers.
Record its complete rationale in the receipt history. The current source
uses the real snapshot observer, the original membership assertions and
the consumed auxiliary fixture, with one parsed YAML display document.

The paused-context public fixture selects input mode `queue` and text mode
`interrupt`. Busy follow-ups enter the runner FIFO because interruption
requires effective input mode `interrupt`. The queued-context consumer
checks retained quoted context, the preceding prompt prefix and eventual
dispatch; its contract does not require adapter debounce or coalescing.
This accepts direct FIFO admission as the adaptation of the former
queued-text fixture. Normal-mode steer cases keep their explicit override.

This records contributor credit and the public fixture adaptation without
changing the source tree.
Native SimpleX chat items include the quoted message separately from
incoming content. The receiver retained that raw payload but omitted the
incoming item ID and quote fields when constructing MessageEvent. The
shared batcher therefore treated replies to different targets as
unquoted input, and the prompt could omit the quoted text.

Populate the existing reply fields before batching or direct dispatch.
Keep the incoming item ID separate from the quoted target. Use a shared
message context identity when the local target ID is unavailable, or an
identity specific to the incoming item when neither target ID exists.
Derive ownership and author data from the quoted direction, including an
unknown group author, and reuse the normaliser for deferred voice input.

Two parameterised native-payload invariants reproduce the missing event
metadata and batching boundaries on the exact prerequisite. They also
check complete prompt framing and deferred file replay on the repair.

Retain the captured UTC timestamp when normalising native SimpleX
reply context. Keep the dataclass import, complete quote normaliser
and incoming identity handling from this contribution.
Native SimpleX reply tests compare complete events and prompts after
the common gateway batches incoming text. The gateway records the
additional message IDs in a merged event and JSON-escapes quoted
text in the prompt. The imported tests omitted those IDs and placed
literal newlines inside the quotation.

Include the second message ID in coalesced expected events and use
JSON encoding for the complete expected quote. Keep the split-event
cases and whole-event and whole-prompt comparisons unchanged.

The canonical 40-case run reproduced 24 merged-event assertion
failures and 16 prompt assertion failures on the unadapted tests.

The native reply additions introduced formatter changes in the reply
context, extended event construction and their fixtures. Format those
scopes and remove the extra blank pair at the fixture insertion boundary.
Keep unrelated inherited formatting unchanged. Complete module ASTs,
parameter populations and assertions remain identical to the accepted
155-case source, so its behaviour evidence remains applicable.
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 6, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 6, 2026
The shared receipt fixture inserts its configured read-receipt and
reaction settings into the Matrix platform block. The unread test also
specified those settings in raw YAML, so composition created duplicate
keys. Configuration parsing failed and the gateway used an unconfigured
provider before the observer could establish the test session.

Select disabled automatic receipts and reactions through the shared
MatrixFeedbackSettings fixture. Keep one platform configuration block
and preserve the original scoped unread and explicit receipt assertions.

The fresh plain and encrypted native run reproduced the duplicate-key
configuration error on the composed parent. No production behaviour or
native expectation changes in this correction.

(cherry picked from commit 2d573c3cb434410412ecfb6c86a8085c798d6d09)

Public source qualification: the complete unread feedback fixture
effect is already present on NousResearch#126398. Positional feedback values match
the original named fields, and the inherited configuration fixture is
exact. This credit changes no source and adds no fresh native acceptance.

Original-source: 2d573c3cb434410412ecfb6c86a8085c798d6d09
The adapter's processing-start hook and topic recovery can await before
message dispatch. A handler installed on entry can disappear during that
await, so the earlier presence check does not make either dispatch call
safe. The turn-marker handoff also had no declared state on MessageEvent.

Declare the marker as an event-local field that resets when dataclasses
replace an event. Select and validate the current handler at each dispatch
boundary. Background selection remains after the start hook and inside the
pending reservation scope. When no handler is installed, background
dispatch reports failure through the existing notification and cleanup
paths. Inline dispatch errors preserve command restoration and clarify
lifecycle handling.

The scoped type comparison removes the three background findings and the
sibling inline optional call, with 48 inherited findings unchanged and no
introduced findings. The canonical lifecycle, marker and withdrawal run
passes 137 cases. The separate command and clarify run passes 19 cases,
including handler removal during real awaited topic recovery. Scoped Ruff
reports no lint findings; unrelated inherited formatter changes remain.
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.

Apply the published fake-API session and reconnect cleanup to this consumer fixture. Preserve its additional Matrix types, membership, encryption and account assertions.
Cached auxiliary-client teardown is mixed into the large routing module.
The worker-loop lifetime correction needs to await only the transports
owned by a terminating worker, without expanding that routing module.

Move the three transport-close functions into a lifecycle sibling and
import the close function at its existing callers. Their full function
ASTs and the remaining routing-module statements are unchanged.
Tool dispatch keeps one event loop per executor thread so cached async
clients can reuse their transports between calls. The thread-local
cache stored only the loop, so ending an executor left the loop and its
selector sockets open. Auxiliary clients also remained cached after
their owner thread exited. Temporary workers used inside an already
running event loop also closed their loops before finishing generators
and cached transports.

Give each worker a Runner owner with a finalizer attached to its
thread-local lifetime. Cancel remaining tasks and finish generators
before closing cached clients on that loop. Wait for their close tasks,
then let Runner shut down the executor and close the loop. Clients that
belong to other live loops remain cached and open.

The regression uses actual executor threads and SDK HTTP connections to
a local server. It checks reuse between calls, generator cleanup before
client closure, closed worker resources, and an unaffected main-thread
client in both worker modes. Timeout cancellation still runs inside the
worker loop, and executor shutdown remains nonblocking for the caller.
The existing fake timeout executor now also closes the test
coroutine which it deliberately never submits.
Matrix turns could read message history, but the agent could not
inspect the current room state, joined members, power levels or pinned
messages. Those details are available from the receiving bot and can
affect what the agent should say or attempt.

Extend the Matrix-only read tool with bounded inspection kinds. Fetch
state and members through the owning adapter, apply its existing room
and requester policy, and resolve pinned messages with the same
decryption path as event reads. Report missing pin content per event
so the other pins remain visible. Include the event type and creator
override when presenting permission thresholds.

A separate Matrix client verifies that the model receives the room
details and pinned message from a disposable Synapse instance.

Use the effective-state parent's client-events decoder and gateway
scheduling path throughout inspection and its tests. Recheck admission
after identity resolution, derive permission defaults from raw state,
and preserve cancellation while refreshing pinned content. The feature
and these interface migrations form one change so the initial commit
uses the current prerequisite APIs.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
The shared receipt fixture inserts its configured read-receipt and
reaction settings into the Matrix platform block. The unread test also
specified those settings in raw YAML, so composition created duplicate
keys. Configuration parsing failed and the gateway used an unconfigured
provider before the observer could establish the test session.

Select disabled automatic receipts and reactions through the shared
MatrixFeedbackSettings fixture. Keep one platform configuration block
and preserve the original scoped unread and explicit receipt assertions.

The fresh plain and encrypted native run reproduced the duplicate-key
configuration error on the composed parent. No production behaviour or
native expectation changes in this correction.

(cherry picked from commit 2d573c3cb434410412ecfb6c86a8085c798d6d09)

Public source qualification: the complete unread feedback fixture
effect is already present on NousResearch#126398. Positional feedback values match
the original named fields, and the inherited configuration fixture is
exact. This credit changes no source and adds no fresh native acceptance.

Original-source: 2d573c3cb434410412ecfb6c86a8085c798d6d09
@iainlane
iainlane force-pushed the feat/matrix-unread branch 2 times, most recently from 7b3a36c to 1e072b0 Compare October 10, 2026 02:42
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 11, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 11, 2026
The complete feedback and edit source adds a second auxiliary-config
argument to the shared native gateway fixture. Native collection fails
before the unread controls can run. Keep the single additive argument
that the controls fixture already provides.

The incoming edit and lifecycle contracts also read optional mock-call
records and assign dynamic fixture fields as statically declared methods.
Check that the required calls occurred before reading their arguments,
use the real profile-hook registry, and bind fixture replacements through
monkeypatch. All original behaviour assertions remain in place.

The complete changed production, unit and native harness type comparison
has no new diagnostics after the defining-owner correspondence checks.
The native collection syntax error is removed; collection-only execution
is diagnostic evidence, not a test pass.

Source and contributor reconciliation for original 726
(1655eb9d3aa2d693af7f8685185c80850408d82d).

The original message above records the composed correction and its
historical checks. This contribution preserves its complete credit after
the source repairs were incorporated into their separate public owners.

The five edit-follow-up fixtures use direct awaited-call guards and the
real ProfileHookRegistries on NousResearch#126312. The lifecycle, deferred-input and
feedback fixtures use the declared typed and monkeypatch adaptations on
NousResearch#126283. These complete unit definitions are also present in its current
composed tree. The native auxiliary configuration fixture has one argument
and its consumer on NousResearch#126398. The NousResearch#126283 native harness has no auxiliary
fixture or consumer, so it requires no corresponding argument.

This is source and credit attribution across those owners. It adds no
product or fixture patch, and records no fresh test, type, SDK or native
execution. The complete original message and both literal contributor
dates are preserved.
@alt-glitch alt-glitch added area/install-update Installer, updater, packaging, wheels, doctor and removed area/docker labels Oct 17, 2026

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

area/config Config system, migrations, profiles area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation 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-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data 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.

7 participants