Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
386 commits
Select commit Hold shift + click to select a range
6980a13
feat(matrix): expose reaction actions in Matrix sessions
iainlane Sep 27, 2026
1c20861
fix(matrix): claim reactions before asynchronous redaction
iainlane Sep 27, 2026
a8a8304
fix(matrix): require an explicit event for agent reactions
iainlane Sep 29, 2026
bcef0b8
fix(matrix): report unreact results from this call's own redactions
iainlane Sep 29, 2026
3c3355f
fix(matrix): bound the record of agent reactions
iainlane Sep 29, 2026
a3dce8f
fix(matrix): check room and requester policy before reaction actions
iainlane Sep 29, 2026
f416f98
fix(matrix): let a timed-out reaction finish and record its event
iainlane Sep 29, 2026
8263130
test(matrix): require reaction actions to run on the gateway loop
iainlane Sep 29, 2026
b2afefc
docs(matrix): state which reactions the agent can remove
iainlane Sep 29, 2026
4e2d29d
test(matrix): bind the gateway loop in the reaction tool tests
iainlane Sep 29, 2026
ff13eed
fix(matrix): apply the reaction deadline on the gateway loop too
iainlane Sep 29, 2026
ee0f156
feat(matrix): configure the Matrix reaction toolset through hermes tools
iainlane Sep 29, 2026
7d7ceca
refactor(matrix): drop the unreachable send_message reaction dispatch
iainlane Sep 29, 2026
12f51c5
fix(matrix): reject reaction actions through unsupported transports
iainlane Oct 1, 2026
7a3aa91
test(matrix): prove captions and filenames through live media
iainlane Sep 27, 2026
5a12b8d
fix(matrix): classify media captions from the wire body
iainlane Sep 27, 2026
b3e1f74
test(matrix): bound media caption reply wait
iainlane Sep 27, 2026
41f2893
fix(matrix): ignore the reply fallback when classifying media captions
iainlane Sep 29, 2026
00da7ce
test(matrix): drop a media test that passes without the fix
iainlane Sep 29, 2026
2ec8953
test(matrix): use a live caption that the legacy heuristic drops
iainlane Sep 29, 2026
c3c46f5
refactor(matrix): extract inbound media filename classification
iainlane Oct 1, 2026
39a284b
fix(matrix): preserve authored quotations in media captions
iainlane Oct 1, 2026
326b6f9
style(matrix): separate media classifiers from filename constants
iainlane Oct 1, 2026
fbae55c
fix(matrix): surface oversized inbound media
ooiuuii Jul 26, 2026
d5ca8ad
fix(matrix): cap inbound media by its downloaded length
iainlane Sep 29, 2026
b4c94e3
test(matrix): import json for the oversized live media test
iainlane Sep 29, 2026
facdb7d
fix(matrix): keep the filename in a too-large marker for a reply
iainlane Sep 29, 2026
9edb6dd
test(matrix): check the media endpoint and token for downloads
iainlane Sep 29, 2026
d7c0f0d
fix(matrix): apply media download limits to quoted images
iainlane Oct 1, 2026
a2cd30a
test(matrix): configure the native media cap through gateway settings
iainlane Oct 1, 2026
6685723
fix(matrix): keep oversized markers compatible with attachment proces…
iainlane Oct 1, 2026
36e26ff
fix(matrix): retain each coalesced native contribution
iainlane Sep 28, 2026
7601dff
test(matrix): build rich-content test events with mautrix
iainlane Sep 29, 2026
070790b
fix(gateway): describe each authored image separately
iainlane Sep 29, 2026
1f60783
fix(matrix): ignore stickers that the server has already redacted
iainlane Sep 29, 2026
e77693d
fix(matrix): strip reply fallbacks from unadmitted native events
iainlane Sep 29, 2026
49d641b
fix(matrix): keep emote attribution out of @ context references
iainlane Sep 29, 2026
10974e2
test(matrix): adapt rich-content tests to the effective-state parent
iainlane Sep 30, 2026
2bc60c3
fix(matrix): apply rich-content withdrawals before the reply line
iainlane Sep 30, 2026
42fc5f2
test(matrix): write rich-content native hand-offs atomically
iainlane Sep 30, 2026
fb470fe
test(matrix): show stickers in explicit room reads
iainlane Sep 30, 2026
8a1f6f3
test(matrix): expect later chain replies to continue the thread
iainlane Sep 30, 2026
43c8dda
test(matrix): accept either completion order for queued stickers
iainlane Sep 30, 2026
6164561
fix(matrix): withdraw rehomed stickers on routed profiles
iainlane Sep 30, 2026
84faf4b
fix(matrix): adapt native snapshots to the effective-state APIs
iainlane Sep 30, 2026
db4e18b
fix(matrix): use current native-content parsing and download paths
iainlane Oct 1, 2026
1272164
refactor(gateway): extract pending merges and redaction handling
iainlane Oct 1, 2026
6ca4f2e
refactor(matrix): extract gateway intake and batch receipts
iainlane Oct 1, 2026
a2106e8
fix(matrix): defer native emote checkpoints until batch admission
iainlane Oct 1, 2026
15fd3dd
test(matrix): wait for the restored intake checkpoint
iainlane Oct 1, 2026
bfafcce
fix(matrix): declare the intake mixin gateway contracts
iainlane Oct 1, 2026
27e2f64
test(matrix): distinguish pending and active rich withdrawal
iainlane Oct 1, 2026
f872fc9
test(gateway): retain contribution identity in busy merge contracts
iainlane Oct 1, 2026
d64cf22
fix(gateway): preserve startup-deferred input at shutdown
iainlane Oct 1, 2026
e43fcad
refactor(gateway): extract busy dispatch and queued follow-up turns
iainlane Oct 1, 2026
084e7f5
refactor(gateway): extract adapter processing ownership
iainlane Oct 1, 2026
ea2eff8
fix(gateway): withdraw pending input until its turn is claimed
iainlane Oct 1, 2026
61082c7
test(gateway): give adapter fixtures concrete pending state
iainlane Oct 1, 2026
94d882b
test(gateway): align shutdown and pause fixtures with current owners
iainlane Oct 2, 2026
84e3257
fix(matrix): preserve native contributions in pending snapshots
iainlane Oct 1, 2026
2bdda85
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
e727cc9
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
43b6ebb
refactor(gateway): extract inbound context preparation
iainlane Oct 1, 2026
3ba63e2
fix(gateway): expand @ references only in the sender's own text
iainlane Sep 29, 2026
4382ab3
refactor(gateway): type extracted quote preparation receivers
iainlane Oct 1, 2026
1b1b16e
refactor(gateway): extract adapter quote and forwarding context
iainlane Oct 1, 2026
792f574
refactor(gateway): type extracted adapter receivers
iainlane Oct 1, 2026
ef4378e
fix(gateway): keep each merged message's channel context
iainlane Sep 30, 2026
3b8d6c2
fix(qqbot): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
8549842
fix(weixin): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
048efad
fix(wecom): keep a quote-only message's quote as reply context
iainlane Sep 30, 2026
edd2402
fix(yuanbao): pass forwarded chat records as channel context
iainlane Sep 30, 2026
18b5bd5
fix(feishu): pass a merged forward as channel context
iainlane Sep 30, 2026
6f350d4
fix(discord): keep forwarded and replied-to text out of the message
iainlane Sep 30, 2026
c3e2310
fix(slack): pass link previews and shared messages as channel context
iainlane Sep 30, 2026
1571e00
fix(telegram): pass forwarded messages as channel context
iainlane Sep 30, 2026
b60a4a0
docs(gateway): say where adapters put quoted and forwarded text
iainlane Sep 30, 2026
4f62d4f
fix(gateway): preserve quotes across ingress text batches
iainlane Oct 1, 2026
0a7d919
test(gateway): wait for the second native stream seed
iainlane Oct 1, 2026
e37e7c2
refactor(yuanbao): import the pipeline adapter type for checking
iainlane Oct 1, 2026
5772eec
test(gateway): use live tasks in adapter cleanup fixtures
iainlane Oct 1, 2026
343d501
feat(matrix): administer rooms through opted-in live sessions
sanastasiou Sep 22, 2026
7383ef6
fix(matrix): refuse administration when a DM becomes a group room
iainlane Sep 30, 2026
722b0c9
fix(matrix): report an unknown outcome when a sent admin write fails
iainlane Sep 30, 2026
2ad3999
fix(matrix): recheck requester power after admin preflight
iainlane Oct 1, 2026
962eea4
fix(matrix): report unknown admin outcomes for JSON response errors
iainlane Oct 1, 2026
c038bb1
fix(matrix): honor MSC2530 filename field for inbound media captions
CocaKova Sep 27, 2026
b5bb102
refactor(gateway): declare composed source and media contracts
iainlane Oct 2, 2026
63076ac
fix(matrix): bound requests when the gateway loop stops
iainlane Oct 1, 2026
1608c48
feat(matrix): list and send image-pack stickers
iainlane Sep 28, 2026
e051824
fix(matrix): revalidate image selections after encryption
iainlane Sep 28, 2026
9ca164c
fix(matrix): accept a compression child during an image-pack turn
iainlane Sep 29, 2026
b0b5f2c
fix(matrix): keep account image packs when room state is too large
iainlane Sep 29, 2026
3eb2c31
fix(matrix): ignore unknown image-pack usage values
iainlane Sep 29, 2026
fc134f2
fix(matrix): read image-pack references once per list
iainlane Sep 29, 2026
bc47400
refactor(matrix): check only image-pack identity that can change
iainlane Sep 29, 2026
bb9922c
test(matrix): assert the exact image-pack refusal for each case
iainlane Sep 29, 2026
524a8b3
fix(matrix): read image-pack state through the shared raw-event helper
iainlane Sep 30, 2026
17bd052
test(matrix): bind the gateway loop in image-pack tool tests
iainlane Sep 30, 2026
5cf69c4
test(matrix): expect a repeated image-pack send to continue the thread
iainlane Sep 30, 2026
b688a95
feat(matrix): configure the image-pack toolset through hermes tools
iainlane Sep 30, 2026
48ef30b
fix(matrix): tell the model to list packs again when an image is removed
iainlane Sep 30, 2026
03a841e
fix(matrix): give image-pack permission checks the raw create event
iainlane Sep 30, 2026
2653750
fix(matrix): read image-pack send state as raw JSON
iainlane Sep 30, 2026
3dc0c4c
fix(matrix): schedule image-pack calls through the shared helper
iainlane Sep 30, 2026
a5bbca7
docs(matrix): list matrix_image_packs among the Matrix agent tools
iainlane Sep 30, 2026
796d5f1
fix(matrix): allow sticker sends when the bot's level is unknown
iainlane Sep 30, 2026
1e536dd
fix(matrix): apply mutation deadlines to image-pack requests
iainlane Oct 1, 2026
f0d758b
test(matrix): verify accepted sticker sends with malformed responses
iainlane Oct 1, 2026
03a527a
feat(matrix): agent-facing add_reaction/remove_reaction (photon parity)
CocaKova Aug 3, 2026
f7c13ab
fix(matrix): drop a queued message when its sender redacts it
iainlane Sep 30, 2026
487042b
fix(gateway): withdraw only the adapter's own platform's messages
iainlane Sep 30, 2026
d9bb494
refactor(matrix): separate redaction parsing from queued withdrawal
iainlane Sep 30, 2026
dde0558
refactor(gateway): extract inbound voice preparation
iainlane Oct 1, 2026
6eceb1c
fix(gateway): preserve voice echo receipts across withdrawal
iainlane Oct 1, 2026
79200e7
test(matrix): use complete session state in pending drain fixtures
iainlane Oct 1, 2026
f39ad28
test(gateway): group withdrawal cases by behaviour contract
iainlane Oct 1, 2026
9c78523
fix(raft): import pending merges from their defining module
iainlane Oct 1, 2026
5ff1fc6
test(gateway): complete the multiplex recovery adapter fixture
iainlane Oct 2, 2026
31e0665
test(matrix): classify the pin room with the current owner API
iainlane Oct 2, 2026
f70f64f
fix(matrix): bound requests when the gateway loop stops
iainlane Oct 1, 2026
2b21575
refactor(telegram): compute the text batch key once per chunk
kshitijk4poor Oct 3, 2026
f374501
fix(telegram): flush reply-boundary batches directly, not via hold queue
kshitijk4poor Oct 3, 2026
1cd0c8c
refactor(telegram): reuse MessageEvent.reply_context for batch bounda…
kshitijk4poor Oct 3, 2026
83ae289
refactor(gateway): share inline-flag padding between busy merges
kshitijk4poor Oct 3, 2026
2ef154b
refactor(gateway): tidy extracted busy-merge modules
kshitijk4poor Oct 3, 2026
dfdc659
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
f76bdb6
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
f72f003
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
d86531a
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
7eda3d2
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
0db474f
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
4af3969
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
06d80e6
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
a9dc27f
fix(gateway): correct types for pending text and queue limits
iainlane Oct 4, 2026
e49946c
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
c94d452
fix(gateway): correct types for pending text and queue limits
iainlane Oct 4, 2026
6b22832
fix(gateway): prevent third-sender message drop in queue-mode debounce
AhmetArif0 May 24, 2026
2e670bd
fix(gateway): queue debounced text behind a slot that cannot absorb it
iainlane Sep 30, 2026
9c72cac
fix(gateway): keep another sender's follow-up out of a pending photo
iainlane Sep 30, 2026
a82e0cf
fix(gateway): queue debounced text behind a slot that cannot absorb it
iainlane Sep 30, 2026
e84e1ea
fix(gateway): keep another sender's follow-up out of a pending photo
iainlane Sep 30, 2026
117767f
fix(gateway): keep queued follow-ups in order when the adapter starts…
iainlane Sep 30, 2026
847f571
docs(gateway): say that only one person's photo burst merges in queue…
iainlane Sep 30, 2026
6509e45
fix(gateway): keep queued follow-ups in order when the adapter starts…
iainlane Sep 30, 2026
61d3e3a
docs(gateway): say that only one person's photo burst merges in queue…
iainlane Sep 30, 2026
8b433f2
fix(gateway): preserve pending event order and context across redispatch
iainlane Oct 1, 2026
92a9cd8
test(gateway): exercise queued reply admission through the adapter
iainlane Oct 1, 2026
615a842
fix(gateway): preserve pending context during busy startup and grace
iainlane Oct 1, 2026
f8821ac
fix(gateway): preserve pending event order and context across redispatch
iainlane Oct 1, 2026
4a938f8
test(gateway): exercise queued reply admission through the adapter
iainlane Oct 1, 2026
8eeb22e
fix(gateway): preserve pending context during busy startup and grace
iainlane Oct 1, 2026
79a4de7
test(telegram): await admission cleanup after cancelled callbacks
iainlane Oct 1, 2026
198140e
test(gateway): cancel a requeue after its back-off task starts
iainlane Oct 1, 2026
4578eea
test(telegram): await admission cleanup after cancelled callbacks
iainlane Oct 1, 2026
e2d0ab8
test(gateway): cancel a requeue after its back-off task starts
iainlane Oct 1, 2026
5f1cb76
refactor(gateway): use a public name for the shared merge predicate
iainlane Oct 2, 2026
fa1d799
test(gateway): combine pending compatibility and FIFO contracts
iainlane Oct 2, 2026
e2dd44b
refactor(gateway): use a public name for the shared merge predicate
iainlane Oct 2, 2026
ccd1727
test(gateway): combine pending compatibility and FIFO contracts
iainlane Oct 2, 2026
63165a1
fix(gateway): enforce bounded FIFO admission and report refusals
iainlane Oct 1, 2026
9cf0c33
fix(gateway): reserve queued heads until a turn claims them
iainlane Oct 1, 2026
70a4810
fix(gateway): retain dispatch claim state after reservation replacement
iainlane Oct 1, 2026
78ddbf5
fix(gateway): reserve queued heads until a turn claims them
iainlane Oct 1, 2026
68efa52
fix(gateway): retain dispatch claim state after reservation replacement
iainlane Oct 1, 2026
f1f2352
fix(i18n): translate the pending queue refusal
iainlane Oct 2, 2026
082c625
fix(i18n): translate the pending queue refusal
iainlane Oct 2, 2026
8584a7c
fix(gateway): preserve transcript spool semantics
Froraut Oct 1, 2026
0e5d29c
fix(gateway): recover identical overlapping transcript snapshots
iainlane Oct 1, 2026
051537d
fix(gateway): recover identical overlapping transcript snapshots
iainlane Oct 1, 2026
e52211a
fix(gateway): preserve complete pending input in shutdown snapshots
iainlane Oct 1, 2026
5f449a1
fix(gateway): preserve complete pending input in shutdown snapshots
iainlane Oct 1, 2026
3ea1085
feat(matrix): resume a reply when its requester reacts
iainlane Sep 27, 2026
3a97e28
fix(matrix): arm streamed replies and defer reaction turns
iainlane Sep 27, 2026
9b14f75
fix(matrix): keep reaction choices through queued final delivery
iainlane Sep 27, 2026
95eabef
fix(matrix): bind reaction watches to delivered replies and sessions
iainlane Sep 27, 2026
2f203a2
fix(matrix): keep reaction follow-ups within admitted conversations
iainlane Sep 27, 2026
ac9d378
fix(matrix): preserve reaction watches through delivery and restart
iainlane Sep 28, 2026
4a12b97
fix(matrix): preserve intake across crypto and handler failures
iainlane Sep 28, 2026
4301ec9
fix(matrix): preserve crypto intake through gateway restarts
iainlane Sep 28, 2026
2da9d2d
fix(matrix): preserve buffered intake and startup replay
iainlane Sep 28, 2026
1082d9f
fix(matrix): acknowledge input that the gateway consumed while busy
iainlane Sep 29, 2026
fa44734
fix(matrix): let text batches span sync responses
iainlane Sep 29, 2026
424bddb
fix(matrix): apply startup grace after a rejected sync cursor
iainlane Sep 29, 2026
31b03f7
fix(matrix): keep reaction watches private and purge them on expiry
iainlane Sep 29, 2026
a616868
fix(matrix): send a final stream edit only for a pending follow-up
iainlane Sep 29, 2026
37c8cb2
fix(gateway): let shutdown notices finish before adapter teardown
iainlane Sep 29, 2026
124439a
test(matrix): pin control precedence over a watched reply
iainlane Sep 29, 2026
657ecf9
test(matrix): leave restart notices out of the encrypted restart case
iainlane Sep 29, 2026
84d06b0
test(matrix): bind the gateway loop in reaction tool tests
iainlane Sep 29, 2026
b474362
test(matrix): give the strict follow-up test's conversation prompt pins
iainlane Sep 29, 2026
0fa8953
test(matrix): build the password-login client as the sync transport does
iainlane Sep 29, 2026
362d2f1
test(matrix): keep line breaks in the expected reply excerpt
iainlane Sep 30, 2026
d3ebe54
test(gateway): return a token list from stubbed session binding
iainlane Sep 30, 2026
48f51c9
fix(matrix): leave a reaction watch armed until a handler can take it
iainlane Sep 30, 2026
b3f8084
test(matrix): pin the server-keyed profile route of a reaction follow-up
iainlane Sep 30, 2026
fe48d40
feat(matrix): configure the follow-up toolset through hermes tools
iainlane Sep 30, 2026
95a115c
fix(matrix): state a reaction follow-up's reaction once in the prompt
iainlane Sep 30, 2026
9f066c4
fix(matrix): keep admitted IDs of the response still being dispatched
iainlane Sep 30, 2026
5bdb2fc
test(gateway): keep the in-flight notice test off the real bound
iainlane Sep 30, 2026
d6608aa
refactor(matrix): list the follow-up toolset apart from reactions
iainlane Sep 30, 2026
1842627
fix(matrix): drain sync checkpoints before clearing them on cancel
iainlane Sep 30, 2026
1c43b4f
fix(matrix): keep a failed dispatch's intake IDs through its retry
iainlane Sep 30, 2026
8d7e543
docs(matrix): say what the cursor keep function returns
iainlane Sep 30, 2026
cc5920b
test(matrix): load the locale catalogue before the native-import stop
iainlane Sep 30, 2026
6c7ba06
test(matrix): align follow-up fixtures with current prerequisites
iainlane Oct 1, 2026
cdd7d1d
refactor(matrix): extract reaction follow-up lifecycle
iainlane Oct 1, 2026
114eb91
fix(matrix): preserve effective logical reaction reply excerpts
iainlane Oct 1, 2026
3f97f43
fix(matrix): consume reaction watches after gateway admission
iainlane Oct 1, 2026
6ba8b71
test(matrix): expect reaction grants through the live auth callback
iainlane Oct 2, 2026
5767d91
test(matrix): cover byte-bounded reaction delivery
iainlane Oct 4, 2026
4b3b062
fix(gateway): coalesce compatible media at recursion limits
iainlane Oct 2, 2026
17a1599
refactor(gateway): extract inbound admission checks
iainlane Oct 1, 2026
7131201
refactor(gateway): extract inbound admission checks
iainlane Oct 1, 2026
6c4763d
fix(gateway): no busy ack for messages deferred by startup restore
iainlane Sep 29, 2026
1367278
fix(gateway): no busy ack for messages deferred by startup restore
iainlane Sep 29, 2026
11e6bf4
fix(gateway): let approval replies and drain notices bypass restore
iainlane Sep 30, 2026
649003b
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
c7ddc86
fix(gateway): let approval replies and drain notices bypass restore
iainlane Sep 30, 2026
934aa12
test(gateway): claim wake inputs at the fake model boundary
iainlane Oct 2, 2026
fa601c3
fix(gateway): preserve transcript spool semantics
Froraut Oct 1, 2026
7dd88be
feat(gateway): reconstruct pending input with withdrawal provenance
iainlane Oct 1, 2026
abf72c0
refactor(gateway): extract turn preparation from agent execution
iainlane Oct 1, 2026
d6bce22
fix(gateway): persist ownership for recursive queued input
iainlane Oct 1, 2026
d4abec8
test(slack): return concrete empty thread context responses
iainlane Oct 1, 2026
fa0a8c3
fix(gateway): type the extracted text batching module
iainlane Oct 1, 2026
8f59e4a
fix(discord): fetch shared history once per text batch
iainlane Oct 1, 2026
b18a175
fix(matrix): capture reply context before authored expansion
iainlane Oct 1, 2026
3d1c1b3
test(gateway): check queued ownership and await stream seeding
iainlane Oct 1, 2026
dcb8853
fix(discord): recheck shared history before publishing a batch
iainlane Oct 1, 2026
2fc5ae7
fix(gateway): expand sender speech before generated context
iainlane Oct 1, 2026
544a874
fix(gateway): retain caption references beside opaque voice caches
iainlane Oct 1, 2026
15fe505
fix(gateway): normalise the opaque caption expansion prefix
iainlane Oct 1, 2026
6e8eb8b
test(gateway): retain authored speech references through pending reco…
iainlane Oct 1, 2026
5741852
docs(gateway): describe channel state from any transcript role
iainlane Oct 1, 2026
7f82ed3
test(gateway): preserve stored context beside authored speech
iainlane Oct 4, 2026
a9622ce
refactor(gateway): extract shared text batching
iainlane Oct 5, 2026
5d32ab7
fix(gateway): keep same-sender busy texts in arrival order
kshitijk4poor Oct 3, 2026
22d4088
fix(gateway): preserve reply targets in shared text batches
iainlane Oct 5, 2026
df0b210
refactor(telegram): extract buffered-delivery teardown
iainlane Oct 5, 2026
86a1e33
fix(telegram): preserve reply-boundary batches during teardown
iainlane Oct 5, 2026
aefaa8a
fix(gateway): prevent third-sender message drop in queue-mode debounce
AhmetArif0 May 24, 2026
8fcb17e
test(telegram): preserve merged identities and callback lifetimes
iainlane Oct 5, 2026
b4227da
fix(gateway): preserve Matrix context as pending snapshot data
iainlane Oct 1, 2026
0cb46df
fix(gateway): preserve leftover steer when pending admission fails
iainlane Oct 5, 2026
08272ef
test(telegram): close the cancelled FIFO fixture coroutine
iainlane Oct 6, 2026
6b95cb6
fix(gateway): enforce bounded FIFO admission and report refusals
iainlane Oct 1, 2026
49c5566
test(matrix): model missing SDK packages in rebind fixtures
iainlane Oct 7, 2026
e0a4a2f
feat(matrix): handle native emotes and stickers
iainlane Sep 28, 2026
1c14b50
fix(matrix): close real sessions in adapter test fixtures
iainlane Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/matrix-live.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Matrix live client

on:
pull_request:
paths:
- '.github/workflows/matrix-live.yml'
- '.github/actions/setup-pm/**'
- '.dockerignore'
- '*.py'
- 'agent/**'
- 'cron/**'
- 'gateway/**'
- 'hermes_cli/**'
- 'hermes_platform/**'
- 'locales/**'
- 'plugins/**'
- 'pm/**'
- 'providers/**'
- 'scripts/bundles/payload.py'
- 'scripts/ci/list_os_marked_tests.py'
- 'scripts/ci/setup_toolchain.py'
- 'scripts/run_tests.sh'
- 'scripts/run_tests_parallel.py'
- 'skills/**'
- 'tests/conftest.py'
- 'tests/fakes/fake_llm_provider.py'
- 'tests/integration/matrix_live/**'
- 'tools/**'
- 'tui_gateway/**'
- 'pyproject.toml'
- 'uv.lock'
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
plain-exchange:
name: Separate client and Linux gateway
runs-on: ubuntu-latest-32-core
timeout-minutes: 45
env:
HERMES_TEST_MATRIX_GATEWAY_IMAGE: hermes-matrix-live:ci
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up locked Python and Matrix dependencies
uses: ./.github/actions/setup-pm
with:
extras: '["matrix"]'
test-environment: 'true'
# Build here so that the per-file test timeout does not have to cover a cold image build.
- name: Build the Linux gateway image
run: '"$HERMES_PYTHON" -m tests.integration.matrix_live.image_build "$HERMES_TEST_MATRIX_GATEWAY_IMAGE"'
- name: Run Matrix live client tests
run: scripts/run_tests.sh --include-integration tests/integration/matrix_live/
env:
HERMES_TEST_WORKERS: '1'
HERMES_TEST_FILE_TIMEOUT: '300'
HERMES_TEST_FILE_RETRIES: '0'
10 changes: 8 additions & 2 deletions agent/context_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from agent.context_compressor_prellm import PreLlmSkipMixin
from agent.context_compressor_summary import SummaryDispatchMixin
from agent.error_classifier import FailoverReason, classify_api_error
from agent.message_metadata import keep_newest_channel_state
from agent.micro_compaction import MicroCompactionMixin
from agent.prompt_builder import STEER_DISPLAY_KIND
from agent.model_metadata import (
Expand Down Expand Up @@ -5745,14 +5746,16 @@ def _assemble_compressed(
summary_role, merge_into_tail, force_user_leading, first_tail_visible_idx = (
self._summary_placement(compressed, tail_messages, compress_start)
)
summary_row = None
if not merge_into_tail:
# End marker stops weak models treating the quoted summary as fresh input (#11475) or
# regurgitating it (#33256).
compressed.append({
summary_row = {
"role": summary_role, "content": summary + "\n\n" + _SUMMARY_END_MARKER,
COMPRESSED_SUMMARY_METADATA_KEY: True,
COMPRESSED_SUMMARY_HAS_USER_TURN_KEY: bool(self._summary_has_user_turn),
})
}
compressed.append(summary_row)
# Default carrier is tail[0]: an exempt row absorbs the summary invisibly. The forced repair
# path needs a non-empty role=user row, so it targets the template-visible row.
merge_target_idx = first_tail_visible_idx if force_user_leading and first_tail_visible_idx is not None else 0
Expand All @@ -5763,7 +5766,10 @@ def _assemble_compressed(
msg[_COMPACTION_TAIL_MARKER] = True
if merge_into_tail and tail_idx == merge_target_idx:
self._merge_summary_into_tail_row(msg, summary, summary_role, force_user_leading)
summary_row = msg
compressed.append(msg)
if summary_row is not None:
keep_newest_channel_state(messages, compressed, summary_row)
return compressed


Expand Down
20 changes: 18 additions & 2 deletions agent/image_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,12 @@ def build_native_content_parts(
user_text: str,
image_paths: list[str],
image_urls: Optional[list[str]] = None,
*,
revalidate: Optional[Callable[[list[str]], tuple[str, list[str]]]] = None,
) -> tuple[list[dict[str, Any]], list[str]]:
"""Build an OpenAI-style ``content`` list for a user turn.

``revalidate`` refreshes text and selects paths after local file conversion.
Local paths become base64 ``data:`` URLs; remote URLs pass through verbatim.
When any image attaches, one text part combines the caption (or a neutral
default) with a ``[Image attached at: <path>]`` / ``[Image attached: <url>]``
Expand All @@ -545,20 +548,33 @@ def build_native_content_parts(
``skipped`` holds unreadable local paths (URLs are never skipped).
"""
skipped: list[str] = []
converted_paths: list[str] = []
attached: list[tuple[str, str]] = [] # (url, hint)
for raw_path in image_paths:
p = Path(raw_path)
data_url = _file_to_data_url(p) if p.exists() and p.is_file() else None
if data_url:
converted_paths.append(raw_path)
attached.append((data_url, f"[Image attached at: {raw_path}]"))
else:
skipped.append(str(raw_path))
attached += [(u, f"[Image attached: {u}]") for u in ((u or "").strip() for u in image_urls or []) if u]
if revalidate is not None:
user_text, retained = revalidate(converted_paths)
attached = [
part for path, part in zip(converted_paths, attached) if path in retained
]
attached += [
(u, f"[Image attached: {u}]")
for u in ((u or "").strip() for u in image_urls or [])
if u
]

text = (user_text or "").strip()
if not attached:
return ([{"type": "text", "text": text}] if text else []), skipped
combined_text = f"{text or 'What do you see in this image?'}\n\n" + "\n".join(h for _, h in attached)
combined_text = f"{text or 'What do you see in this image?'}\n\n" + "\n".join(
h for _, h in attached
)
image_parts = [{"type": "image_url", "image_url": {"url": u}} for u, _ in attached]
return [{"type": "text", "text": combined_text}, *image_parts], skipped

Expand Down
32 changes: 31 additions & 1 deletion agent/message_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,35 @@ def message_identity(msg: MutableMapping[str, Any], *, with_tool_uids: bool = Fa
return identity


# The ``display_metadata`` entry in which the gateway saves a snapshot of the chat (its name, topic and
# similar state) on a user row. The newest row with one is the snapshot that the conversation has
# acknowledged, so a rewrite that removes that row must record the snapshot on a row that it keeps.
CHANNEL_STATE_METADATA_KEY = "channel_state"


def _channel_state(msg: Any) -> Optional[dict]:
metadata = msg.get("display_metadata") if isinstance(msg, Mapping) else None
state = metadata.get(CHANNEL_STATE_METADATA_KEY) if isinstance(metadata, Mapping) else None
return state if isinstance(state, dict) else None


def newest_channel_state(messages: List[Any]) -> Optional[dict]:
"""The channel state saved on the newest row of *messages* that has one."""
return next((state for msg in reversed(messages) if (state := _channel_state(msg)) is not None), None)


def keep_newest_channel_state(
original: List[Any], rewritten: List[Any], carrier: MutableMapping[str, Any],
) -> None:
"""Record the newest channel state of *original* on *carrier* when *rewritten* no longer has it as
its newest. *carrier* is the row of *rewritten* that replaces the removed rows (a compaction summary
or a merge survivor), and every row after it in *rewritten* must be a row of *original*."""
state = newest_channel_state(original)
if state is None or newest_channel_state(rewritten) == state:
return
carrier["display_metadata"] = {**(carrier.get("display_metadata") or {}), CHANNEL_STATE_METADATA_KEY: state}


def record_absorbed_message(
survivor: MutableMapping[str, Any], dropped: Mapping[str, Any], *, dropped_leads: bool = False,
) -> None:
Expand All @@ -123,8 +152,9 @@ def record_absorbed_message(
text leads; with *dropped_leads* the dropped dict's text was put first (the real user anchor folded
into a scaffolding turn), so its uid becomes the survivor's and the survivor's former uid is recorded.
A dict without a uid (unflushed, scaffolding, engine-authored) contributes nothing; an empty result
leaves the survivor untouched.
leaves the survivor untouched. The survivor also keeps the newest channel state of the two constituents.
"""
keep_newest_channel_state([dropped, survivor] if dropped_leads else [survivor, dropped], [survivor], survivor)
survivor_uid = message_uid_or_none(survivor)
dropped_uid = message_uid_or_none(dropped)
own = uid_list(survivor.get(ABSORBED_MESSAGE_UIDS))
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/desktop-toolsets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const DESKTOP_HIDDEN_TOOLSETS = new Set([
// adapter; `hermes tools` restricts these off the CLI too.
'discord',
'discord_admin',
'matrix_admin',
'yuanbao',
// Internal plumbing, not a user capability toggle.
'context_engine',
Expand Down
8 changes: 5 additions & 3 deletions gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ gateway under the backend, and do NOT "fix" update locks by widening the tree-ki
answers "which bot received it / who may admit it / where does it run" ONCE per event and pins a
frozen `RoutingIdentity` on the source (wire-invisible, like `_transport_adapter_ref`). Every
ingress path calls the canonicalize seam before it derives a key: the adapter side
(`platforms/base.py::_canonicalize` — `handle_message`, `_enqueue_text_event`, Telegram photo /
album routing, `_handle_message_while_active`, every `_source_session_key`) and the runner side
(`platforms/base.py::_canonicalize` in `handle_message`, Telegram photo /
album routing and every `_source_session_key`;
`platforms/base_busy.py::_handle_message_while_active`; and
`platforms/base_text_batching.py::_enqueue_text_event`) and the runner side
(`run_adapters.py::_canonicalize` — the per-profile / default message, busy and platform-event
handlers, the adapter auth-check callback, `run_inbound.py::_hm_admit_event`). No key derivation
handlers, the adapter auth-check callback, `run_inbound_admission.py::_hm_admit_event`). No key derivation
before it; an unresolved identity under multiplexing (route to an unserved profile) is dropped
with one WARNING at the first seam it reaches, never keyed into `agent:main`. `_transport_owner`,
`_authorization_home_for_source`, `_resolve_profile_home_for_source`, `_session_key_profile` and
Expand Down
155 changes: 155 additions & 0 deletions gateway/inbound_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Refreshable external context for a new gateway input."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass, field
from typing import Any, Callable, Protocol, TypeVar

from gateway.platforms.event import MessageEvent, TurnContextUpdate
from gateway.session import SessionSource

_T = TypeVar("_T")

_LOOP_CALL_TIMEOUT_SECONDS = 10.0


class InboundContextSnapshot(Protocol):
def pending_state(self) -> dict[str, Any]: ...

def use_turn_context(self, update: TurnContextUpdate | None) -> None: ...

async def refresh(self) -> None: ...

def prepend_turn_context(self, text: str) -> str: ...

def reply_event(self, event: MessageEvent) -> MessageEvent: ...

def reply_image_paths(self) -> list[str]: ...

def media_event(self, event: MessageEvent) -> MessageEvent:
"""Return *event* with only the authored attachments that are still current."""
...

def authored_text(self, text: str) -> str:
"""Return the prepared message *text* with each authored contribution replaced by its
current text."""
...


@dataclass(frozen=True)
class ImageEnrichment:
path: str
text: str

@classmethod
async def enrich_each(
cls, runner: Any, source: SessionSource, session_key: str, paths: list[str]
) -> tuple[ImageEnrichment, ...]:
"""Enrich each image separately so that withdrawing one image removes only its own
description. The session's native image buffer keeps its paths and adds each path
routed natively here."""
native_images = runner._consume_pending_native_image_paths(session_key)
enrichments = []
for path in paths:
text = await runner._enrich_inbound_images(source, session_key, "", [path])
enrichments.append(cls(path, text))
native_images.extend(runner._consume_pending_native_image_paths(session_key))
state = runner._peek_session_state(session_key)
if state is not None:
state.persistent.native_image_paths = list(dict.fromkeys(native_images))
return tuple(enrichments)


@dataclass
class PreparedInboundMessage:
"""A new input whose external context is rendered again before model use.

The snapshot reads platform state that belongs to the event loop that
prepared the input. Calls from any other thread run on that loop.
"""

snapshot: InboundContextSnapshot
event: MessageEvent
text: str
redact_pii: bool = False
quoted_images: tuple[ImageEnrichment, ...] = ()
authored_images: tuple[ImageEnrichment, ...] = ()
message_text: str | None = None
persist_user_message: str | None = None
persist_user_timestamp: float | None = None
loop: asyncio.AbstractEventLoop = field(
default_factory=asyncio.get_running_loop, repr=False, compare=False
)

def retained_image_paths(self, paths: list[str]) -> list[str]:
return self._on_loop(self._retained_image_paths, paths)

def revalidate_native_input(
self, runner: Any, message: str, paths: list[str]
) -> tuple[str, list[str]]:
return self._on_loop(self._revalidate_native_input, runner, message, paths)

def render(self, runner: Any, *, timestamps: bool = False) -> str:
return self._on_loop(self._render, runner, timestamps)

def _on_loop(self, call: Callable[..., _T], *args: Any) -> _T:
try:
running = asyncio.get_running_loop()
except RuntimeError:
running = None
if running is self.loop or not self.loop.is_running():
return call(*args)

async def run() -> _T:
return call(*args)

future = asyncio.run_coroutine_threadsafe(run(), self.loop)
try:
return future.result(timeout=_LOOP_CALL_TIMEOUT_SECONDS)
except TimeoutError:
future.cancel()
raise

def _retained_image_paths(self, paths: list[str]) -> list[str]:
current = self.snapshot.reply_image_paths()
authored = self.event.authored_media().media_urls
current_authored = self.snapshot.media_event(self.event).media_urls
quoted = {image.path for image in self.quoted_images}
return [
path
for path in paths
if (path not in quoted or path in current or path in current_authored)
and (path not in authored or path in current_authored or path in current)
]

def _revalidate_native_input(
self, runner: Any, message: str, paths: list[str]
) -> tuple[str, list[str]]:
previous = self.message_text
current = self._render(runner, True)
if previous is not None and previous in message:
current = message.replace(previous, current, 1)
return current, self._retained_image_paths(paths)

def _render(self, runner: Any, timestamps: bool) -> str:
text = self.snapshot.authored_text(self.text)
authored = self.snapshot.media_event(self.event).media_urls
quoted = self.snapshot.reply_image_paths()
descriptions = [
*(image.text for image in self.authored_images if image.path in authored and image.text),
*(image.text for image in self.quoted_images if image.path in quoted and image.text),
]
if descriptions:
text = "\n\n".join([*descriptions, text])
reply = self.snapshot.reply_event(self.event)
text = runner._prepend_inbound_reply_context(
reply, self.event.source, text, redact_pii=self.redact_pii,
)
text = self.snapshot.prepend_turn_context(text)
if timestamps:
text, self.persist_user_message, self.persist_user_timestamp = (
runner._hmwa_apply_message_timestamp(self.event, text)
)
self.message_text = text
return text
18 changes: 18 additions & 0 deletions gateway/input_owner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Stable transcript ownership for accepted gateway input."""

import json
import uuid

from gateway.platforms.event import MessageEvent
from gateway.session import SessionSource


def gateway_input_owner(event: MessageEvent | None, source: SessionSource) -> str:
uid = getattr(event, "_pending_snapshot_uid", None)
message_id = getattr(event, "message_id", None)
identifier = f"pending:{uid}" if uid else str(message_id) if message_id else None
if identifier is None:
return str(uuid.uuid4())
namespace = [source.platform.value, source.profile, source.scope_id,
source.chat_id, source.thread_id, identifier]
return str(uuid.uuid5(uuid.NAMESPACE_URL, json.dumps(namespace)))
Loading