Skip to content

fix(gateway): keep reply context when busy follow-ups merge - #129160

Draft
iainlane wants to merge 272 commits into
NousResearch:mainfrom
iainlane:fix/gateway-busy-merge-reply-context
Draft

iainlane wants to merge 272 commits into
NousResearch:mainfrom
iainlane:fix/gateway-busy-merge-reply-context

Conversation

@iainlane

@iainlane iainlane commented Sep 30, 2026 •

Copy link
Copy Markdown

Stack

Nesting records logical merge order. #129160 follows both #126281 and #135482. The worker-loop repair in #135482 is independent and can merge at any time; its commits are not duplicated in this branch.

Change

Busy follow-ups preserve reply targets and quoted content when chunks combine. Compatible reply contexts merge; conflicting explicit contexts create separate batches. Shared batching retains arrival order, the latest inbound message identity, all contributor IDs, attachment captions, aligned inline-media flags and context dependencies. This covers normalised Discord, Matrix, WhatsApp, SimpleX and WeCom events.

Telegram resolves each batching key once and dispatches reply-boundary batches through tracked tasks. Cancellation and reconnect teardown preserve both the earlier boundary event and the pending event. Shared batching and delayed Telegram delivery are extracted into topical modules.

The six adopted follow-ups from #132243 retain their complete original contribution metadata. Integration preserves the upstream media and context absorption helpers in the extracting commits.

Current composition and validation

I resolved the concrete extraction conflicts against the fixed upstream revision 3637c512fc3211bdccb61b7140d09ac1018508ba and reapplied this layer onto the repaired #126281 supplier. The debounce state and batching methods remain in their existing topical modules. The same upstream unquoted annotations are applied inside those modules. Three bounded joins occur in contributions one, seven and fourteen; every other complete join is automatic. All sixteen complete author and committer headers, timestamps, timezones, original message prefixes, order and the empty thirteenth record are preserved. The supplier's checkpoint, admission, room, real HTTP-session and store teardown repairs remain intact.

All 172 assertions pass across the nine complete own-layer files. Seven children, with 146 assertions, have clean resource shutdown. The other two complete children pass 26 assertions but emit twelve database shutdown warnings. Both complete test modules are byte-identical to the preceding published #129160 source, whose complete child outputs have the same warning counts. These warnings are reported separately; passing assertions do not establish clean shutdown for those two files.

The Matrix and reaction gate uses the unchanged three-record #135482 prerequisite at its captured public revision a4027e0661448650ff40cdd7cda0269c3127a9fc. All 311 assertions pass across both complete children, with no unclosed database, event loop, socket or transport. Those three records are a logical prerequisite and are not incorporated into this feature branch.

Both gates use the canonical runner, the installed Matrix SDK, four workers, zero retries and four strict warning categories. Full child output through interpreter shutdown is retained. All 8,470 pure-branch inputs and 8,472 composed inputs are authenticated against their exact runtime Git objects before and after execution. The runner is restored exactly. The final source differs from the tested source by formatting one complete batching module and removing one surplus EOF newline from an unused supplier test module. Both complete module ASTs are identical, and all selected runtime and type-check source inputs remain unchanged apart from the already checked batching formatting. These qualified scopes account for 483 passing assertions across eleven complete files, with 457 assertions in nine children that have clean resource shutdown.

The twenty-two-path type comparison has 632 findings. Of these, 628 complete lexical and defining-statement operands, relative spans, columns and occurrence counts match the preceding public or corrected supplier scope. Four existing WeCom media override findings have separate whole-source qualification: their diagnostic headers and exact spans already occur, both complete defining methods are unchanged, the complete current media module equals the supplier, and the actual bases are identical. The complete enclosing class AST differs because this feature adds unrelated batching methods, so this is not a claim that every complete class AST matches. The four media contracts introduce no new finding. A fresh check of the solely formatted batching module preserves its complete diagnostic operands. Scoped Ruff and introduced whitespace checks pass. Eighteen inherited formatter paths remain, with no new path. These checks do not establish global static cleanliness.

Local joins with the fixed verified campaign base and required upstream revision are clean. Earlier native and wider-gate receipts remain evidence for their recorded source revisions. No fresh live Matrix exchange or hosted CI execution is claimed.

History and review

Published head: 627fb2227c402f619e699eb2afd9214dd6e9180d. Review the sixteen own commits above the actual published supplier. The branch preserves the original contribution credit from #132243. All corrections to supplier-owned fixtures remain in their supplier contributions.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/telegram Telegram bot adapter sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 30, 2026
Matrix permalinks change with each triggering event. Rendering one in
the pinned session prompt made the prompt stale or changed its bytes
every turn, which breaks conversation prompt caching.

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

Preserve the Matrix and Discord source-note handling when integrating
the upstream prefix removal change. Use removeprefix after selecting
the platform-specific note, so each transport removes only its own
generated prefix.
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.
Matrix compares the room with the snapshot saved on the newest user
row that has one, under display_metadata["channel_state"]. Compaction
replaced the summarised rows with a summary row that had no snapshot.
When the summarised rows included every snapshot newer than the
protected head, the baseline became an older snapshot in the head (the
first turn's, with the default protect_first_n) or the session origin
once the head no longer protects any user row. The next turn then
repeated a name or topic note that the model had already seen, and it
compared member and security state against stale or unknown values.

Compaction removes the rows that contain the baseline, so compaction now
keeps it. keep_newest_channel_state in
agent/message_metadata.py records the newest snapshot of the original
rows on the summary row (or on the tail row that absorbs the summary)
when the compacted rows no longer have it as their newest. Tail rows
come after the summary row, so a newer snapshot in the tail still
wins. record_absorbed_message, which every fold of one row into
another calls, applies the same rule, so the survivor of a merge of
adjacent user rows (micro-compaction, alternation repair) keeps the
later row's snapshot.

The summary row can be an assistant row, so the gateway now reads the
newest snapshot on a row of any role. CHANNEL_STATE_METADATA_KEY moves
to agent/message_metadata.py because the compressor needs it, and
acknowledged_channel_state is replaced by newest_channel_state.
When the compressor merges its summary into a tail row instead of
adding a standalone row, that tail row must take the newest channel
state from the rows it replaces. No test reached this path, so
dropping the carrier assignment in _assemble_compressed passed the
suite.

Call _assemble_compressed directly for a summary merged into tail[0],
for one merged into the first visible tail row when the head is empty,
and for a tail that already has a newer snapshot, which must win.
Matrix changes lacked a live exchange test, so mocked adapter tests
could miss failures between Synapse, gateway setup, and model-visible
context.

Start disposable Synapse and a checkout-built Linux gateway through
Testcontainers. Drive events with a separate matrix-nio account and a
fake model under an isolated HERMES_HOME. Provision a Linux nio client
with its own encrypted device store so later encrypted cases can also
run from macOS hosts.

Run the suite in a dedicated CI job through scripts/run_tests.sh. The
plain-room test confirms both the reply and the inbound model context.
Synapse changes the owner of /data to UID 991 when it generates its
configuration. On a Linux runner, the host test process cannot append
to the resulting homeserver.yaml. Store Synapse data in a Docker volume
and edit the configuration from a one-shot container.

The test runner can kill a timed-out file worker before pytest tears
down its fixtures. Enable Testcontainers' Ryuk reaper, pin its image by
digest, and start it before creating the volume. Label the Synapse
volume with the session ID; Testcontainers labels its network and
containers. Ryuk can then remove these resources when the worker exits.
Parallel Matrix live tests built different checkouts under the same
hermes-matrix-live:checkout tag. A second build could replace the tag
between the first build and its container start, so a green test might
exercise the wrong code.

Give each fixture invocation a UUID image tag and remove that exact tag
after its containers stop. Concurrent branches can now run the live
suite without changing which checkout an existing fixture starts.
A file timeout kills the pytest worker and its build process without
running fixture teardown. The unique image tag from the live test can
then remain on the Docker host indefinitely.

Start Ryuk before building and label the checkout image with the same
Testcontainers session ID as its containers, network and volume. Ryuk
can remove the image after the worker dies. A SIGKILL probe confirmed
that the pinned Ryuk image removes a labelled Docker image.
Matrix live tests built the production image for each checkout. That
build prepares SQLite, Chromium, Node and the UI even though the tests
start only a gateway. The separate Linux observer also built a second
Python environment at test setup time to obtain matrix-nio crypto.

Build a small Debian image with the PM-pinned Python and the locked
Matrix and test dependencies before copying the checkout source. The
gateway and observer use the same prepared interpreter in separate
containers, while the gateway still runs against an isolated home and
the host fake model. Keep gateway logs in timeout failures so startup
problems remain diagnosable.

The plain-room test passed with a 1.545-second test body and 26.8
seconds for the complete test file. On this host, an uncached image
build took 128.1 seconds and a cached rebuild took 4.67 seconds.
The live plain-room test requested a one-second Synapse poll and
allowed a minute before reporting a missing reply. Matrix-nio can
retry a sync request beyond that loop deadline, so a stalled
exchange could take even longer to fail.

Request a 250 ms server wait and bound each sync by the remaining
15-second deadline. Successful replies still arrive through the
real Matrix event stream. The separate-client Synapse test passed.
The approval client sends cards, seed reactions and requester controls in
quick succession. Synapse's default account event limit exhausts its
burst and rejects the controls before the short client deadline.

Configure the disposable server for 100 events per second with a burst
of 100. The limit remains finite, and tests can exercise protocol events
without waiting for the production rate limiter to refill. Client waits,
retry rules and assertions are unchanged.
The Matrix live fixtures skipped with "require a running Docker
daemon" on macOS installations whose daemon is reachable only through
a Docker CLI context, such as Docker Desktop or Colima without the
/var/run/docker.sock symlink. The fixture replaced DOCKER_CONFIG with
an empty configuration to keep registry credentials away from the
tests. That also discarded currentContext and the context metadata, so
the Docker SDK, Testcontainers and the docker CLI all fell back to
/var/run/docker.sock. scripts/run_tests.sh removed DOCKER_HOST,
DOCKER_CONTEXT and DOCKER_CONFIG from the test environment, so a user
could not select the socket explicitly either.

The fixture now resolves the endpoint the same way the docker CLI
does (DOCKER_HOST, then DOCKER_CONTEXT, then currentContext) before it
isolates the configuration. It exports the result as DOCKER_HOST and
removes DOCKER_CONTEXT, so the SDK probe, Testcontainers and the
buildx build all connect to that endpoint. The isolated configuration
keeps the CLI plugin directories, because Docker Desktop and Homebrew
install buildx outside the default search path. It still contains no
registry credentials or credential helpers.

On macOS the daemon runs in a Linux VM and resolves bind-mount sources
there. Testcontainers mounts the client-side socket path into Ryuk by
default, which fails when that path is a host socket such as a
context's. The fixture therefore mounts /var/run/docker.sock, the
socket inside the VM, into Ryuk on macOS.

The runner forwards DOCKER_HOST, DOCKER_CONTEXT and DOCKER_CONFIG. It
still drops DOCKER_AUTH_CONFIG, DOCKER_CERT_PATH and DOCKER_TLS_VERIFY.

With a scratch context whose socket forwards to OrbStack and counts
connections, the plain exchange passed with 70 API connections through
that socket. The previous fixture passed the same invocation with no
connections through it, because it used /var/run/docker.sock instead.
A context pointing at a missing socket now skips with a message that
includes the socket path.
The last dependency step of the test Dockerfile imports olm,
mautrix.crypto and nio, so an image without Matrix crypto support
already fails to build, and the fixture reports the build log. The
fixture then started a container from the finished image only to run
the same imports again. That check could not fail, and it added a
container start to every test module.
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 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.
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.
The dependency-rebinding fixtures installed a fake importer but left the
package-availability check unchanged. With a complete SDK installed,
ensure_matrix_deps skips the importer, so these fixtures failed without
exercising their intended success and refusal paths.

Report a missing Matrix package in the fixture alongside the existing
installation seam. The real rebind and failed-import assertions then
exercise the intended fresh-install boundary.
The Matrix adapter ignores emotes and does not register native sticker
events. Emotes describe the sender's action, while stickers use a
separate event type with a description and an image. The existing text
handler would interpret a slash-like action body as a gateway command,
so native actions need attributed text before command classification.

Accept both through the existing room, sender and mention gates. Prefix
emotes with their actor, preserve sticker descriptions, and download
sticker pixels through the bounded media path. Use the same labels in
effective reads, replies and thread history.

Queued stickers can be edited or redacted while media preparation is
pending. Add an optional media-event snapshot capability and retain the
source paths of image descriptions, so final model preparation removes
both withdrawn pixels and their derived text. Existing platform
snapshots remain compatible and previous conversation context remains
unchanged.

The two parameterised invariants reproduce six failures on the committed
parents and now cover native SDK events, gates, unavailable media and
withdrawal during asynchronous preparation. Matrix and adjacent gateway
checks pass against the pinned SDK source.
@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch from e613dbb to 471fa57 Compare October 7, 2026 18:34
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 7, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.

@kvnloo kvnloo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped review of the documented own layer (5724ccfd..471fa579, 16 commits): no blocking finding in the reply-context/batching changes. Shared adapters now split on conflicting explicit reply contexts and preserve compatible/plain+reply context, contributor message ids, attachment order and inline flags; the tests exercise those cases across Discord/Matrix/WhatsApp/SimpleX/WeCom. Telegram’s separate boundary buffer keeps the earlier batch tracked through cancellation/reconnect instead of losing it after the quiet-period task is replaced. The busy debounce regressions also pin the three-message order/reply-context cases. This is not a clean bill for the whole 187-file stacked view: #126281 remains a prerequisite, and the known cross-sender merge/cap work stays separately scoped in dependent #129126/#130445.

@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch from 471fa57 to 4302fbe Compare October 8, 2026 08:20
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 8, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch from 4302fbe to 276b0f7 Compare October 9, 2026 05:37
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.
Pending-event merges and busy-text debounce share reply and attachment
semantics inside the large base adapter. Telegram's ingress overrides
also modify the same batching boundary. Extract those existing topics
before integrating the reviewed reply-context fixes.

Repoint in-tree imports and patch targets to the defining merge module.
The adapter classes inherit their existing methods, so timers, tracked
redispatch and delayed-delivery ownership keep their current behaviour.

The fixed upstream annotation cleanup overlaps the extracted debounce
state. Keep the state in its existing topical module and apply the same
unquoted task annotation there. The complete state and timer behaviour
remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch from 276b0f7 to 1defc7b Compare October 9, 2026 07:50
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch from 1defc7b to cbd73a4 Compare October 9, 2026 09:15
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
@iainlane
iainlane force-pushed the fix/gateway-busy-merge-reply-context branch 2 times, most recently from 4f5f1cf to 627fb22 Compare October 9, 2026 21:14
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 9, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.
iainlane added a commit to iainlane/hermes-agent that referenced this pull request Oct 10, 2026
Debounced text could bypass the runner's merge checks and inherit the
sender or control permissions of a pending photo. Photo coalescing also
moved later captions ahead of earlier queued messages and discarded a
conflicting reply target.

Use one predicate for sender, control and plugin metadata, and reply
compatibility. Submit debounced bursts through bounded FIFO admission,
and coalesce a photo burst only while the overflow is empty. Standalone
adapters keep incompatible bursts in their existing debounce state so a
third sender or reply remains independent. The normal drain, discard and
shutdown paths process or preserve those events; cap refusal leaves the
previously accepted events intact.

An attempted FIFO head could be deferred again while the next event was
already staged. Appending the attempted event rotated the queue. Track
the adapter's dispatch within its current task and restore that event to
the front when the handler queues it again. Source and message identity
also recognise rewrite copies, with timestamps for events without IDs.
The existing retry backoff still applies, while unrelated arrivals keep
their place at the end of the queue.

Independent probes reproduced permission inheritance, photo overtaking,
reply loss and FIFO rotation on the prior head. The repaired contracts
and adjacent tests pass, including rewrite copies, reconnect backoff and
standalone drain, discard, shutdown and cap refusal.

The preceding extraction moved the debounce state and methods into
gateway/platforms/base_text_debounce.py. Apply these declaration and
method changes there, retaining typed receivers and explicit field
imports. This source replaces the earlier shared sender functions with
its new base_pending.py declarations. Remove their old defining-sibling
copies and update consumers to the new defining imports. Keep the
upstream selected-owner loop and its staging call; apply only the
original acceptance assignment in that handler. The runner uses its
existing defining-module merge-helper import.

Maintenance adaptation: remove the surplus terminal blank line from the
extracted debounce module. All implementation syntax remains unchanged.

Parent alignment: retain the inherited same-sender a/b/a arrival case
from NousResearch#129160. FIFO preserves all three reply contexts, so the expected
turns remain separate a, b and a events. Its input, case ID and whole
event assertion remain unchanged.

This branch has not been deployed

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

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.