Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
394 commits
Select commit Hold shift + click to select a range
1cba115
feat(matrix): expose reaction actions in Matrix sessions
iainlane Sep 27, 2026
52e05b6
fix(matrix): claim reactions before asynchronous redaction
iainlane Sep 27, 2026
adc18cb
fix(matrix): require an explicit event for agent reactions
iainlane Sep 29, 2026
7273274
fix(matrix): report unreact results from this call's own redactions
iainlane Sep 29, 2026
32c399a
fix(matrix): bound the record of agent reactions
iainlane Sep 29, 2026
3209727
fix(matrix): check room and requester policy before reaction actions
iainlane Sep 29, 2026
26e6334
fix(matrix): let a timed-out reaction finish and record its event
iainlane Sep 29, 2026
67ff741
test(matrix): require reaction actions to run on the gateway loop
iainlane Sep 29, 2026
99cab27
docs(matrix): state which reactions the agent can remove
iainlane Sep 29, 2026
b76982c
test(matrix): bind the gateway loop in the reaction tool tests
iainlane Sep 29, 2026
d3fcbb2
fix(matrix): apply the reaction deadline on the gateway loop too
iainlane Sep 29, 2026
f51218b
feat(matrix): configure the Matrix reaction toolset through hermes tools
iainlane Sep 29, 2026
c5ce3d4
refactor(matrix): drop the unreachable send_message reaction dispatch
iainlane Sep 29, 2026
61f2aee
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
c51ef20
fix(matrix): retain each coalesced native contribution
iainlane Sep 28, 2026
ab1e450
test(matrix): build rich-content test events with mautrix
iainlane Sep 29, 2026
4cd8126
fix(gateway): describe each authored image separately
iainlane Sep 29, 2026
b0fae88
fix(matrix): ignore stickers that the server has already redacted
iainlane Sep 29, 2026
10aabc6
fix(matrix): strip reply fallbacks from unadmitted native events
iainlane Sep 29, 2026
3719c16
fix(matrix): keep emote attribution out of @ context references
iainlane Sep 29, 2026
0f444cf
test(matrix): adapt rich-content tests to the effective-state parent
iainlane Sep 30, 2026
ec3eed1
fix(matrix): apply rich-content withdrawals before the reply line
iainlane Sep 30, 2026
8697188
test(matrix): write rich-content native hand-offs atomically
iainlane Sep 30, 2026
b20721c
test(matrix): show stickers in explicit room reads
iainlane Sep 30, 2026
e66b527
test(matrix): expect later chain replies to continue the thread
iainlane Sep 30, 2026
0b6e5a0
test(matrix): accept either completion order for queued stickers
iainlane Sep 30, 2026
e9e848f
fix(matrix): withdraw rehomed stickers on routed profiles
iainlane Sep 30, 2026
a7e3837
fix(matrix): adapt native snapshots to the effective-state APIs
iainlane Sep 30, 2026
3559964
fix(matrix): use current native-content parsing and download paths
iainlane Oct 1, 2026
ce60ad0
refactor(gateway): extract pending merges and redaction handling
iainlane Oct 1, 2026
73f96f0
refactor(matrix): extract gateway intake and batch receipts
iainlane Oct 1, 2026
683b7cb
fix(matrix): defer native emote checkpoints until batch admission
iainlane Oct 1, 2026
bf14e1e
test(matrix): wait for the restored intake checkpoint
iainlane Oct 1, 2026
a8cf4f1
fix(matrix): declare the intake mixin gateway contracts
iainlane Oct 1, 2026
f9039b8
test(matrix): distinguish pending and active rich withdrawal
iainlane Oct 1, 2026
d5f0288
test(gateway): retain contribution identity in busy merge contracts
iainlane Oct 1, 2026
a0297a0
fix(gateway): preserve startup-deferred input at shutdown
iainlane Oct 1, 2026
2144e2d
refactor(gateway): extract busy dispatch and queued follow-up turns
iainlane Oct 1, 2026
2a39a13
refactor(gateway): extract adapter processing ownership
iainlane Oct 1, 2026
39bdcf8
fix(gateway): withdraw pending input until its turn is claimed
iainlane Oct 1, 2026
b204e88
test(gateway): give adapter fixtures concrete pending state
iainlane Oct 1, 2026
6d224a0
test(gateway): align shutdown and pause fixtures with current owners
iainlane Oct 2, 2026
34dc3ef
fix(matrix): preserve native contributions in pending snapshots
iainlane Oct 1, 2026
7f5fb20
test(gateway): complete the multiplex recovery adapter fixture
iainlane Oct 2, 2026
8e9a3a6
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
d13d782
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
e9b16ce
refactor(gateway): extract inbound context preparation
iainlane Oct 1, 2026
ab0c27e
fix(gateway): expand @ references only in the sender's own text
iainlane Sep 29, 2026
7adfb58
refactor(gateway): type extracted quote preparation receivers
iainlane Oct 1, 2026
ceb39ae
refactor(gateway): extract adapter quote and forwarding context
iainlane Oct 1, 2026
cbc8d41
refactor(gateway): type extracted adapter receivers
iainlane Oct 1, 2026
a609711
fix(gateway): keep each merged message's channel context
iainlane Sep 30, 2026
26f26b4
fix(qqbot): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
743ff36
fix(weixin): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
66acfa0
fix(wecom): keep a quote-only message's quote as reply context
iainlane Sep 30, 2026
fc74bdd
fix(yuanbao): pass forwarded chat records as channel context
iainlane Sep 30, 2026
57d2e13
fix(feishu): pass a merged forward as channel context
iainlane Sep 30, 2026
3eb9eb4
fix(discord): keep forwarded and replied-to text out of the message
iainlane Sep 30, 2026
aeba78b
fix(slack): pass link previews and shared messages as channel context
iainlane Sep 30, 2026
b6c3dcc
fix(telegram): pass forwarded messages as channel context
iainlane Sep 30, 2026
13d7fcc
docs(gateway): say where adapters put quoted and forwarded text
iainlane Sep 30, 2026
93802ef
fix(gateway): preserve quotes across ingress text batches
iainlane Oct 1, 2026
c3f7709
test(gateway): wait for the second native stream seed
iainlane Oct 1, 2026
077e544
refactor(yuanbao): import the pipeline adapter type for checking
iainlane Oct 1, 2026
f84b084
test(gateway): use live tasks in adapter cleanup fixtures
iainlane Oct 1, 2026
9ce2465
fix(gateway): withdraw earlier independent debounce inputs
iainlane Oct 1, 2026
0deec17
fix(gateway): withdraw pending Discord and Slack deletions
iainlane Oct 1, 2026
e431a9e
fix(discord): withdraw auto-thread input from its origin channel
iainlane Oct 2, 2026
40f57b3
test(gateway): type native deletion fixtures against the SDK
iainlane Oct 2, 2026
e2422c7
fix(slack): validate normalised edits before replacing the event
iainlane Oct 2, 2026
fe97dd5
refactor(gateway): declare composed source and media contracts
iainlane Oct 2, 2026
0a86815
fix(matrix): bound requests when the gateway loop stops
iainlane Oct 1, 2026
06fef50
feat(matrix): list and send image-pack stickers
iainlane Sep 28, 2026
88b7527
fix(matrix): revalidate image selections after encryption
iainlane Sep 28, 2026
e3189a0
fix(matrix): accept a compression child during an image-pack turn
iainlane Sep 29, 2026
e5955fb
fix(matrix): keep account image packs when room state is too large
iainlane Sep 29, 2026
f3cb603
fix(matrix): ignore unknown image-pack usage values
iainlane Sep 29, 2026
d9c4c53
fix(matrix): read image-pack references once per list
iainlane Sep 29, 2026
9fc3a6c
refactor(matrix): check only image-pack identity that can change
iainlane Sep 29, 2026
cdcaba3
test(matrix): assert the exact image-pack refusal for each case
iainlane Sep 29, 2026
9834f51
fix(matrix): read image-pack state through the shared raw-event helper
iainlane Sep 30, 2026
02eda98
test(matrix): bind the gateway loop in image-pack tool tests
iainlane Sep 30, 2026
b7370be
test(matrix): expect a repeated image-pack send to continue the thread
iainlane Sep 30, 2026
69d330b
feat(matrix): configure the image-pack toolset through hermes tools
iainlane Sep 30, 2026
7de900b
fix(matrix): tell the model to list packs again when an image is removed
iainlane Sep 30, 2026
134efef
fix(matrix): give image-pack permission checks the raw create event
iainlane Sep 30, 2026
0d01a3a
fix(matrix): read image-pack send state as raw JSON
iainlane Sep 30, 2026
b33b2f8
fix(matrix): schedule image-pack calls through the shared helper
iainlane Sep 30, 2026
43dd288
docs(matrix): list matrix_image_packs among the Matrix agent tools
iainlane Sep 30, 2026
4cc5922
fix(matrix): allow sticker sends when the bot's level is unknown
iainlane Sep 30, 2026
574cad7
fix(matrix): apply mutation deadlines to image-pack requests
iainlane Oct 1, 2026
c395613
test(matrix): verify accepted sticker sends with malformed responses
iainlane Oct 1, 2026
c6be379
feat(matrix): agent-facing add_reaction/remove_reaction (photon parity)
CocaKova Aug 3, 2026
7592c7b
fix(matrix): drop a queued message when its sender redacts it
iainlane Sep 30, 2026
562a98c
fix(gateway): withdraw only the adapter's own platform's messages
iainlane Sep 30, 2026
82c9ed5
refactor(matrix): separate redaction parsing from queued withdrawal
iainlane Sep 30, 2026
0a1bfb4
refactor(gateway): extract inbound voice preparation
iainlane Oct 1, 2026
bf9e7b7
fix(gateway): preserve voice echo receipts across withdrawal
iainlane Oct 1, 2026
7a1527c
test(matrix): use complete session state in pending drain fixtures
iainlane Oct 1, 2026
ce691a9
test(gateway): group withdrawal cases by behaviour contract
iainlane Oct 1, 2026
d07502f
fix(raft): import pending merges from their defining module
iainlane Oct 1, 2026
bad22dc
test(gateway): complete the multiplex recovery adapter fixture
iainlane Oct 2, 2026
70a750a
refactor(telegram): compute the text batch key once per chunk
kshitijk4poor Oct 3, 2026
d5bb802
fix(telegram): flush reply-boundary batches directly, not via hold queue
kshitijk4poor Oct 3, 2026
ab78fb9
refactor(telegram): reuse MessageEvent.reply_context for batch bounda…
kshitijk4poor Oct 3, 2026
77eb511
refactor(gateway): share inline-flag padding between busy merges
kshitijk4poor Oct 3, 2026
4685b43
refactor(gateway): tidy extracted busy-merge modules
kshitijk4poor Oct 3, 2026
ec75c45
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
395d70e
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
06f1f85
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
4e038f5
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
0a92200
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
fad70bc
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
e8299de
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
e132a0b
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
61fda6f
fix(gateway): correct types for pending text and queue limits
iainlane Oct 4, 2026
c49be60
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
eac78d3
fix(gateway): correct types for pending text and queue limits
iainlane Oct 4, 2026
219a93e
fix(gateway): prevent third-sender message drop in queue-mode debounce
AhmetArif0 May 24, 2026
ae05fca
fix(gateway): queue debounced text behind a slot that cannot absorb it
iainlane Sep 30, 2026
96726e1
fix(gateway): keep another sender's follow-up out of a pending photo
iainlane Sep 30, 2026
ac3335e
fix(gateway): queue debounced text behind a slot that cannot absorb it
iainlane Sep 30, 2026
818ec43
fix(gateway): keep another sender's follow-up out of a pending photo
iainlane Sep 30, 2026
c293bde
fix(gateway): keep queued follow-ups in order when the adapter starts…
iainlane Sep 30, 2026
a97bf81
docs(gateway): say that only one person's photo burst merges in queue…
iainlane Sep 30, 2026
0b5136e
fix(gateway): keep queued follow-ups in order when the adapter starts…
iainlane Sep 30, 2026
a1b14ee
docs(gateway): say that only one person's photo burst merges in queue…
iainlane Sep 30, 2026
547ae4f
fix(gateway): preserve pending event order and context across redispatch
iainlane Oct 1, 2026
4be6f7d
test(gateway): exercise queued reply admission through the adapter
iainlane Oct 1, 2026
c691c30
fix(gateway): preserve pending context during busy startup and grace
iainlane Oct 1, 2026
073ed80
fix(gateway): preserve pending event order and context across redispatch
iainlane Oct 1, 2026
2557595
test(gateway): exercise queued reply admission through the adapter
iainlane Oct 1, 2026
693301c
fix(gateway): preserve pending context during busy startup and grace
iainlane Oct 1, 2026
736b0ee
test(telegram): await admission cleanup after cancelled callbacks
iainlane Oct 1, 2026
47ba2a9
test(gateway): cancel a requeue after its back-off task starts
iainlane Oct 1, 2026
29ea17c
test(telegram): await admission cleanup after cancelled callbacks
iainlane Oct 1, 2026
56255e8
test(gateway): cancel a requeue after its back-off task starts
iainlane Oct 1, 2026
8563074
refactor(gateway): use a public name for the shared merge predicate
iainlane Oct 2, 2026
d6d6694
test(gateway): combine pending compatibility and FIFO contracts
iainlane Oct 2, 2026
d4d7784
refactor(gateway): use a public name for the shared merge predicate
iainlane Oct 2, 2026
f50a018
test(gateway): combine pending compatibility and FIFO contracts
iainlane Oct 2, 2026
358a37e
fix(gateway): enforce bounded FIFO admission and report refusals
iainlane Oct 1, 2026
7592d99
fix(gateway): reserve queued heads until a turn claims them
iainlane Oct 1, 2026
681beca
fix(gateway): retain dispatch claim state after reservation replacement
iainlane Oct 1, 2026
259a9f6
fix(gateway): reserve queued heads until a turn claims them
iainlane Oct 1, 2026
edd6b4a
fix(gateway): retain dispatch claim state after reservation replacement
iainlane Oct 1, 2026
d5d1102
fix(i18n): translate the pending queue refusal
iainlane Oct 2, 2026
4319c7d
fix(i18n): translate the pending queue refusal
iainlane Oct 2, 2026
00a02a8
fix(gateway): preserve transcript spool semantics
Froraut Oct 1, 2026
9126fdf
fix(gateway): recover identical overlapping transcript snapshots
iainlane Oct 1, 2026
51753c6
fix(gateway): recover identical overlapping transcript snapshots
iainlane Oct 1, 2026
a7a5a81
fix(gateway): preserve complete pending input in shutdown snapshots
iainlane Oct 1, 2026
a7a884e
fix(gateway): preserve complete pending input in shutdown snapshots
iainlane Oct 1, 2026
4d0bc36
feat(matrix): resume a reply when its requester reacts
iainlane Sep 27, 2026
e76ecbe
fix(matrix): arm streamed replies and defer reaction turns
iainlane Sep 27, 2026
4464ebb
fix(matrix): keep reaction choices through queued final delivery
iainlane Sep 27, 2026
632c4a8
fix(matrix): bind reaction watches to delivered replies and sessions
iainlane Sep 27, 2026
c4f05ca
fix(matrix): keep reaction follow-ups within admitted conversations
iainlane Sep 27, 2026
fbb146e
fix(matrix): preserve reaction watches through delivery and restart
iainlane Sep 28, 2026
9a3e6fb
fix(matrix): preserve intake across crypto and handler failures
iainlane Sep 28, 2026
9cff896
fix(matrix): preserve crypto intake through gateway restarts
iainlane Sep 28, 2026
b7c5e7f
fix(matrix): preserve buffered intake and startup replay
iainlane Sep 28, 2026
58ec684
fix(matrix): acknowledge input that the gateway consumed while busy
iainlane Sep 29, 2026
0cef50f
fix(matrix): let text batches span sync responses
iainlane Sep 29, 2026
c035baf
fix(matrix): apply startup grace after a rejected sync cursor
iainlane Sep 29, 2026
8f3d838
fix(matrix): keep reaction watches private and purge them on expiry
iainlane Sep 29, 2026
202da01
fix(matrix): send a final stream edit only for a pending follow-up
iainlane Sep 29, 2026
10cb179
fix(gateway): let shutdown notices finish before adapter teardown
iainlane Sep 29, 2026
dfb7b00
test(matrix): pin control precedence over a watched reply
iainlane Sep 29, 2026
a5f18db
test(matrix): leave restart notices out of the encrypted restart case
iainlane Sep 29, 2026
32867b8
test(matrix): bind the gateway loop in reaction tool tests
iainlane Sep 29, 2026
c706a10
test(matrix): give the strict follow-up test's conversation prompt pins
iainlane Sep 29, 2026
3d45f20
test(matrix): build the password-login client as the sync transport does
iainlane Sep 29, 2026
f94687c
test(matrix): keep line breaks in the expected reply excerpt
iainlane Sep 30, 2026
d23ad35
test(gateway): return a token list from stubbed session binding
iainlane Sep 30, 2026
e60aebc
fix(matrix): leave a reaction watch armed until a handler can take it
iainlane Sep 30, 2026
8b138d0
test(matrix): pin the server-keyed profile route of a reaction follow-up
iainlane Sep 30, 2026
2f730ad
feat(matrix): configure the follow-up toolset through hermes tools
iainlane Sep 30, 2026
b356615
fix(matrix): state a reaction follow-up's reaction once in the prompt
iainlane Sep 30, 2026
1863ce8
fix(matrix): keep admitted IDs of the response still being dispatched
iainlane Sep 30, 2026
6bb4f57
test(gateway): keep the in-flight notice test off the real bound
iainlane Sep 30, 2026
16d490d
refactor(matrix): list the follow-up toolset apart from reactions
iainlane Sep 30, 2026
83108df
fix(matrix): drain sync checkpoints before clearing them on cancel
iainlane Sep 30, 2026
96a1a3b
fix(matrix): keep a failed dispatch's intake IDs through its retry
iainlane Sep 30, 2026
a3fc07e
docs(matrix): say what the cursor keep function returns
iainlane Sep 30, 2026
fed010e
test(matrix): load the locale catalogue before the native-import stop
iainlane Sep 30, 2026
2c39df5
test(matrix): align follow-up fixtures with current prerequisites
iainlane Oct 1, 2026
08c5b77
refactor(matrix): extract reaction follow-up lifecycle
iainlane Oct 1, 2026
a9bf70d
fix(matrix): preserve effective logical reaction reply excerpts
iainlane Oct 1, 2026
894daa3
fix(matrix): consume reaction watches after gateway admission
iainlane Oct 1, 2026
f86144d
test(matrix): expect reaction grants through the live auth callback
iainlane Oct 2, 2026
09a74a7
test(matrix): cover byte-bounded reaction delivery
iainlane Oct 4, 2026
9488066
fix(gateway): coalesce compatible media at recursion limits
iainlane Oct 2, 2026
befe9d5
refactor(gateway): extract inbound admission checks
iainlane Oct 1, 2026
3defe1a
refactor(gateway): extract inbound admission checks
iainlane Oct 1, 2026
de7a95e
fix(gateway): no busy ack for messages deferred by startup restore
iainlane Sep 29, 2026
223cea3
fix(gateway): no busy ack for messages deferred by startup restore
iainlane Sep 29, 2026
da7d092
fix(gateway): let approval replies and drain notices bypass restore
iainlane Sep 30, 2026
88f046e
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
80c2d6c
fix(gateway): let approval replies and drain notices bypass restore
iainlane Sep 30, 2026
cff40ee
test(gateway): claim wake inputs at the fake model boundary
iainlane Oct 2, 2026
17e38df
fix(gateway): preserve transcript spool semantics
Froraut Oct 1, 2026
814260d
feat(gateway): reconstruct pending input with withdrawal provenance
iainlane Oct 1, 2026
b509db1
refactor(gateway): extract turn preparation from agent execution
iainlane Oct 1, 2026
509d35b
fix(gateway): persist ownership for recursive queued input
iainlane Oct 1, 2026
231b869
test(slack): return concrete empty thread context responses
iainlane Oct 1, 2026
40e8799
fix(gateway): type the extracted text batching module
iainlane Oct 1, 2026
646fcdb
fix(discord): fetch shared history once per text batch
iainlane Oct 1, 2026
5d64dae
fix(matrix): capture reply context before authored expansion
iainlane Oct 1, 2026
b368288
test(gateway): check queued ownership and await stream seeding
iainlane Oct 1, 2026
394623b
fix(discord): recheck shared history before publishing a batch
iainlane Oct 1, 2026
e583511
fix(gateway): expand sender speech before generated context
iainlane Oct 1, 2026
35904e1
fix(gateway): retain caption references beside opaque voice caches
iainlane Oct 1, 2026
7efc6fb
fix(gateway): normalise the opaque caption expansion prefix
iainlane Oct 1, 2026
c05431f
test(gateway): retain authored speech references through pending reco…
iainlane Oct 1, 2026
e88e00d
docs(gateway): describe channel state from any transcript role
iainlane Oct 1, 2026
129004c
test(gateway): preserve stored context beside authored speech
iainlane Oct 4, 2026
0e7b815
refactor(gateway): extract shared text batching
iainlane Oct 5, 2026
73d4d5d
fix(gateway): keep same-sender busy texts in arrival order
kshitijk4poor Oct 3, 2026
fdb86f8
fix(gateway): preserve reply targets in shared text batches
iainlane Oct 5, 2026
392d942
refactor(telegram): extract buffered-delivery teardown
iainlane Oct 5, 2026
c805e1f
fix(telegram): preserve reply-boundary batches during teardown
iainlane Oct 5, 2026
407c117
fix(gateway): prevent third-sender message drop in queue-mode debounce
AhmetArif0 May 24, 2026
f58c8a1
test(telegram): preserve merged identities and callback lifetimes
iainlane Oct 5, 2026
93fc8e9
fix(gateway): preserve Matrix context as pending snapshot data
iainlane Oct 1, 2026
29bf3d3
fix(gateway): preserve leftover steer when pending admission fails
iainlane Oct 5, 2026
ad45cd2
test(telegram): close the cancelled FIFO fixture coroutine
iainlane Oct 6, 2026
d1707f9
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
a0f377d
feat(matrix): handle native emotes and stickers
iainlane Sep 28, 2026
2a4880a
test(discord): construct notification thread fixtures without arguments
iainlane Oct 7, 2026
5365f40
fix(matrix): close real sessions in adapter test fixtures
iainlane Oct 9, 2026
6852e14
refactor(gateway): extract platform notification and intake topics
iainlane Oct 1, 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