Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
478 commits
Select commit Hold shift + click to select a range
cc4c635
fix(matrix): bridge process_edits config.yaml key to its env var
chelsealong Sep 5, 2026
7f4e6f1
fix(matrix): queue opt-in edit corrections at turn boundaries
iainlane Sep 28, 2026
c3f4e22
fix(matrix): keep queued corrections apart from debounced text
iainlane Sep 29, 2026
84f38da
fix(matrix): leave thread fallback and receipts alone for corrections
iainlane Sep 29, 2026
0de546a
fix(matrix): put correction lifecycle reactions on the original message
iainlane Sep 29, 2026
959619b
test(matrix): detect an environment bridge for process_edits
iainlane Sep 29, 2026
c037fda
docs(matrix): describe how corrections are queued and answered
iainlane Sep 29, 2026
ab860ea
fix(matrix): unpack the seven-field message context for corrections
iainlane Sep 30, 2026
59b839d
test(matrix): prepare corrections through the current turn-context path
iainlane Sep 30, 2026
87103c3
refactor(matrix): import Method from the shared client-events module
iainlane Sep 30, 2026
80d2a29
fix(matrix): queue a correction when the runner finds the session busy
iainlane Sep 30, 2026
8d5d177
test(matrix): expect a thread continuation for a queued correction reply
iainlane Sep 30, 2026
bc65a01
fix(gateway): queue debounced text when the pending slot cannot absor…
iainlane Sep 30, 2026
fae8a55
test(matrix): keep text sent between two corrections before the second
iainlane Sep 30, 2026
1926e53
test(matrix): keep a pill-quoted correction as typed
iainlane Sep 30, 2026
05f139c
docs(matrix): describe correction replies in threads and outside them
iainlane Sep 30, 2026
cca73b4
fix(matrix): verify moderator redactions before pending withdrawal
iainlane Oct 2, 2026
4c8dc8e
fix(gateway): withdraw original input from durable pending snapshots
iainlane Oct 2, 2026
a10c986
fix(gateway): serialise mutations of pending snapshot files
iainlane Oct 2, 2026
7627bd2
fix(gateway): preserve native pending content and attachment identity
iainlane Oct 2, 2026
8f2e9eb
refactor(gateway): extract prepared turn execution
iainlane Oct 2, 2026
d349c0f
test(gateway): claim heartbeat input at the fake execution boundary
iainlane Oct 2, 2026
3203f44
fix(matrix): validate native pending input before replay
iainlane Oct 2, 2026
77ba705
fix(gateway): bind restored turns to their durable input owner
iainlane Oct 2, 2026
a08a743
fix(gateway): restore verified pending records before newer startup i…
iainlane Oct 2, 2026
a3dc1d8
refactor(gateway): describe pending restoration interfaces
iainlane Oct 2, 2026
6ac7517
fix(gateway): verify restored input authority before cached media reads
iainlane Oct 2, 2026
e639278
refactor(discord): separate native input preparation from dispatch
iainlane Oct 2, 2026
6125ed1
refactor(discord): separate guild access policy
iainlane Oct 2, 2026
cf5cbc1
fix(gateway): restore Discord input under current native authority
iainlane Oct 2, 2026
8dca694
fix(gateway): restore completion origins in their owning profile
iainlane Oct 1, 2026
d0c42c6
test(gateway): pass the pending key to busy profile controls
iainlane Oct 2, 2026
bf778cb
feat(matrix): opt-in processing of message edits as new agent turns
chelsealong Sep 5, 2026
75edfcc
refactor(matrix): move reply preparation beside inbound admission
iainlane Oct 2, 2026
c736636
fix(matrix): give corrections to the model as typed
iainlane Sep 29, 2026
cfe0e85
fix(matrix): recheck correction policy after context preparation
iainlane Oct 1, 2026
2dac9ba
fix(matrix): apply current notice policy to corrections
iainlane Oct 1, 2026
4daa542
fix(gateway): keep turn-boundary input out of photo coalescing
iainlane Oct 1, 2026
73f6165
refactor(gateway): move source profile resolution into its owner
iainlane Oct 2, 2026
ffb7d9c
refactor(gateway): extract profile-owned shutdown recovery
iainlane Oct 1, 2026
ff3f172
fix(gateway): consume pending records only after user execution
iainlane Oct 1, 2026
3fd8d84
refactor(slack): separate input preparation from dispatch
iainlane Oct 2, 2026
cb73b26
refactor(slack): move thread hydration beside input preparation
iainlane Oct 2, 2026
6baf566
fix(gateway): restore Slack input from current native evidence
iainlane Oct 2, 2026
7e75de1
fix(gateway): restore accepted input at its original FIFO position
iainlane Oct 2, 2026
290a50a
fix(gateway): preserve native file provenance across routed caches
iainlane Oct 2, 2026
98ea95d
fix(matrix): project rich pending input from its routed file identity
iainlane Oct 2, 2026
55ae783
fix(matrix): revalidate persisted corrections before replay
iainlane Oct 2, 2026
eaca16f
test(matrix): verify pending corrections through gateway restart
iainlane Oct 2, 2026
eb5ed2e
fix(gateway): remap pending voice receipts with routed media
iainlane Oct 2, 2026
fb64739
fix(gateway): retain file identities in generated voice notes
iainlane Oct 2, 2026
2e2acb1
test(matrix): retain native pending restoration and moderation contracts
iainlane Oct 2, 2026
12b01f8
fix(gateway): describe each authored image separately
iainlane Sep 29, 2026
4857929
fix(matrix): ignore stickers that the server has already redacted
iainlane Sep 29, 2026
2fe0faa
fix(matrix): strip reply fallbacks from unadmitted native events
iainlane Sep 29, 2026
2dfe36b
fix(matrix): keep emote attribution out of @ context references
iainlane Sep 29, 2026
a203e7a
test(matrix): adapt rich-content tests to the effective-state parent
iainlane Sep 30, 2026
aae3e5b
fix(matrix): apply rich-content withdrawals before the reply line
iainlane Sep 30, 2026
00fd3b0
test(matrix): write rich-content native hand-offs atomically
iainlane Sep 30, 2026
6f3efc8
test(matrix): show stickers in explicit room reads
iainlane Sep 30, 2026
e88279f
test(matrix): expect later chain replies to continue the thread
iainlane Sep 30, 2026
9f362f6
test(matrix): accept either completion order for queued stickers
iainlane Sep 30, 2026
9429332
fix(matrix): withdraw rehomed stickers on routed profiles
iainlane Sep 30, 2026
f9f7be2
fix(matrix): adapt native snapshots to the effective-state APIs
iainlane Sep 30, 2026
0c3638a
fix(matrix): use current native-content parsing and download paths
iainlane Oct 1, 2026
c83a2b2
fix(matrix): bound requests when the gateway loop stops
iainlane Oct 1, 2026
c700687
feat(matrix): list and send image-pack stickers
iainlane Sep 28, 2026
c112a51
fix(matrix): revalidate image selections after encryption
iainlane Sep 28, 2026
017c603
fix(matrix): accept a compression child during an image-pack turn
iainlane Sep 29, 2026
c62bb55
fix(matrix): keep account image packs when room state is too large
iainlane Sep 29, 2026
f2da451
fix(matrix): ignore unknown image-pack usage values
iainlane Sep 29, 2026
d9cdf72
fix(matrix): read image-pack references once per list
iainlane Sep 29, 2026
d135b01
refactor(matrix): check only image-pack identity that can change
iainlane Sep 29, 2026
73f8eb5
test(matrix): assert the exact image-pack refusal for each case
iainlane Sep 29, 2026
1539961
fix(matrix): read image-pack state through the shared raw-event helper
iainlane Sep 30, 2026
e40f553
test(matrix): bind the gateway loop in image-pack tool tests
iainlane Sep 30, 2026
2ab2a77
test(matrix): expect a repeated image-pack send to continue the thread
iainlane Sep 30, 2026
291666d
feat(matrix): configure the image-pack toolset through hermes tools
iainlane Sep 30, 2026
043c48a
fix(matrix): tell the model to list packs again when an image is removed
iainlane Sep 30, 2026
82a0e95
fix(matrix): give image-pack permission checks the raw create event
iainlane Sep 30, 2026
91f4f95
fix(matrix): read image-pack send state as raw JSON
iainlane Sep 30, 2026
e93785d
fix(matrix): schedule image-pack calls through the shared helper
iainlane Sep 30, 2026
26c46a7
docs(matrix): list matrix_image_packs among the Matrix agent tools
iainlane Sep 30, 2026
5b67e2a
fix(matrix): allow sticker sends when the bot's level is unknown
iainlane Sep 30, 2026
3b06d47
fix(matrix): apply mutation deadlines to image-pack requests
iainlane Oct 1, 2026
55b319a
test(matrix): verify accepted sticker sends with malformed responses
iainlane Oct 1, 2026
7c783ba
fix(raft): import pending merges from their defining module
iainlane Oct 1, 2026
5f77692
test(matrix): import pending merges from their defining module
iainlane Oct 1, 2026
040894f
fix(matrix): preserve native contributions in pending snapshots
iainlane Oct 1, 2026
ed0c984
refactor(matrix): extract gateway intake and batch receipts
iainlane Oct 1, 2026
a926591
fix(matrix): defer native emote checkpoints until batch admission
iainlane Oct 1, 2026
9c08b33
test(matrix): wait for the restored intake checkpoint
iainlane Oct 1, 2026
81f807b
fix(matrix): declare the intake mixin gateway contracts
iainlane Oct 1, 2026
6b1e04e
test(matrix): distinguish pending and active rich withdrawal
iainlane Oct 1, 2026
9b6294d
test(gateway): retain contribution identity in busy merge contracts
iainlane Oct 1, 2026
f5bf327
fix(gateway): preserve startup-deferred input at shutdown
iainlane Oct 1, 2026
9e1ef24
refactor(gateway): extract busy dispatch and queued follow-up turns
iainlane Oct 1, 2026
510843f
refactor(gateway): extract adapter processing ownership
iainlane Oct 1, 2026
0f8b63f
fix(gateway): withdraw pending input until its turn is claimed
iainlane Oct 1, 2026
c367281
test(gateway): give adapter fixtures concrete pending state
iainlane Oct 1, 2026
adf5f48
refactor(gateway): extract pending merges and Telegram text batching
iainlane Oct 1, 2026
f4ba957
refactor(gateway): type extracted reply adapter receivers
iainlane Oct 1, 2026
1c9c406
refactor(gateway): extract inbound context preparation
iainlane Oct 1, 2026
cd6e1dc
fix(gateway): expand @ references only in the sender's own text
iainlane Sep 29, 2026
b0d31ff
refactor(gateway): type extracted quote preparation receivers
iainlane Oct 1, 2026
2a5ff87
refactor(gateway): extract adapter quote and forwarding context
iainlane Oct 1, 2026
2bd8e35
refactor(gateway): type extracted adapter receivers
iainlane Oct 1, 2026
6329870
fix(gateway): keep each merged message's channel context
iainlane Sep 30, 2026
d6f0f0d
fix(qqbot): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
a447fc8
fix(weixin): pass a quoted message as reply context, not as body text
iainlane Sep 30, 2026
afd28f9
fix(wecom): keep a quote-only message's quote as reply context
iainlane Sep 30, 2026
a99b034
fix(yuanbao): pass forwarded chat records as channel context
iainlane Sep 30, 2026
1b3f998
fix(feishu): pass a merged forward as channel context
iainlane Sep 30, 2026
3349069
fix(discord): keep forwarded and replied-to text out of the message
iainlane Sep 30, 2026
17ab016
fix(slack): pass link previews and shared messages as channel context
iainlane Sep 30, 2026
492ff61
fix(telegram): pass forwarded messages as channel context
iainlane Sep 30, 2026
0430886
docs(gateway): say where adapters put quoted and forwarded text
iainlane Sep 30, 2026
69d3b45
fix(gateway): preserve quotes across ingress text batches
iainlane Oct 1, 2026
e52772c
test(gateway): wait for the second native stream seed
iainlane Oct 1, 2026
fe59a8c
refactor(yuanbao): import the pipeline adapter type for checking
iainlane Oct 1, 2026
a7e207b
test(gateway): use live tasks in adapter cleanup fixtures
iainlane Oct 1, 2026
d08a6c8
feat(gateway): reconstruct pending input with withdrawal provenance
iainlane Oct 1, 2026
5bb550a
refactor(gateway): extract turn preparation from agent execution
iainlane Oct 1, 2026
7190f45
fix(gateway): persist ownership for recursive queued input
iainlane Oct 1, 2026
f552b50
test(slack): return concrete empty thread context responses
iainlane Oct 1, 2026
816d8e9
fix(gateway): type the extracted text batching module
iainlane Oct 1, 2026
d554979
fix(discord): fetch shared history once per text batch
iainlane Oct 1, 2026
10906e7
fix(matrix): capture reply context before authored expansion
iainlane Oct 1, 2026
5b56854
test(gateway): check queued ownership and await stream seeding
iainlane Oct 1, 2026
b561b47
fix(discord): recheck shared history before publishing a batch
iainlane Oct 1, 2026
af76413
fix(gateway): expand sender speech before generated context
iainlane Oct 1, 2026
1faee10
fix(gateway): retain caption references beside opaque voice caches
iainlane Oct 1, 2026
5f313d1
fix(gateway): normalise the opaque caption expansion prefix
iainlane Oct 1, 2026
bd96c3b
test(gateway): retain authored speech references through pending reco…
iainlane Oct 1, 2026
d2f2995
docs(gateway): describe channel state from any transcript role
iainlane Oct 1, 2026
8db35e2
fix(gateway): withdraw earlier independent debounce inputs
iainlane Oct 1, 2026
1f051b3
fix(gateway): withdraw pending Discord and Slack deletions
iainlane Oct 1, 2026
43ca62e
fix(discord): withdraw auto-thread input from its origin channel
iainlane Oct 2, 2026
339e71d
test(gateway): type native deletion fixtures against the SDK
iainlane Oct 2, 2026
fcd16a8
refactor(gateway): move pending media placeholders beside cache routing
iainlane Oct 1, 2026
a51fe43
fix(gateway): route quoted media before rendering pending input
iainlane Oct 1, 2026
2ed23bc
fix(gateway): dispatch approval controls during startup restoration
iainlane Oct 2, 2026
af7da4e
refactor(telegram): extract photo and album batching
iainlane Oct 2, 2026
1b01fae
fix(gateway): preserve distinct reply targets in ingress batches
iainlane Oct 2, 2026
657e0a8
fix(gateway): restore accepted follow-ups at the recursion cap
iainlane Oct 2, 2026
be775c6
refactor(gateway): extract inbound context preparation
iainlane Oct 1, 2026
7e99536
fix(gateway): keep reply quotes inside one quoted value
iainlane Sep 30, 2026
f2db2a1
fix(gateway): describe both sources of a quoted reply
iainlane Sep 30, 2026
cb86d50
refactor(gateway): type extracted quote preparation receivers
iainlane Oct 1, 2026
c3324bb
fix(gateway): preserve reply context in busy follow-ups
mazzanfar Sep 21, 2026
1aa77ea
fix(gateway): preserve standalone follow-up reply boundaries
iainlane Oct 2, 2026
d71044c
refactor(gateway): extract inbound reply diagnostics
iainlane Oct 2, 2026
41933b8
fix(gateway): queued/steered follow-ups keep the reply-to prefix
Sahilvishnaliya Sep 3, 2026
6dc51cc
review: extract reply-context projection to bounded owner
Sahilvishnaliya Sep 3, 2026
40e772a
fix: restore _platform_name for the response-ready log line
Sahilvishnaliya Sep 3, 2026
196b0b2
refactor(gateway): declare reply projection owners
iainlane Oct 2, 2026
1eb2c8e
refactor(gateway): move adapter intake to its processing owner
iainlane Oct 2, 2026
e4497df
fix(gateway): order shutdown input across pending stores
iainlane Oct 2, 2026
457ac0f
fix(gateway): release turn ownership before marker cleanup
iainlane Oct 1, 2026
acb41ac
refactor(gateway): separate processing and execution lifecycles
iainlane Oct 2, 2026
d456671
feat(matrix): configurable read receipts + lifecycle reactions
ZoloZiak Sep 24, 2026
44520bb
refactor(matrix): extract inbound media handling
iainlane Oct 1, 2026
5de7f85
fix(matrix): scope receipt and reaction settings to each profile
iainlane Sep 28, 2026
9342204
fix(matrix): acknowledge all inputs processed by a turn
iainlane Sep 28, 2026
e9fe125
fix(matrix): acknowledge completed input at processing boundaries
iainlane Sep 28, 2026
d4dcbda
fix(matrix): classify cooperative interruption before feedback
iainlane Sep 28, 2026
c1c87a2
fix(matrix): keep receipts with accepted processing input
iainlane Sep 28, 2026
145377c
fix(matrix): complete queued feedback after final delivery
iainlane Sep 28, 2026
f47c43a
fix(matrix): defer steering after model completion
iainlane Sep 28, 2026
5f9fcbe
fix(gateway): complete a turn superseded by new input with its chain
iainlane Sep 29, 2026
25f097f
fix(gateway): complete busy input that the priority path folds in
iainlane Sep 29, 2026
8e9e655
fix(gateway): keep inline acknowledgements out of the control paths
iainlane Sep 29, 2026
8ed37d9
fix(gateway): keep failed turns on the delivery outcome
iainlane Sep 29, 2026
3c29c59
test(matrix): drive receipt chains through the real runner
iainlane Sep 29, 2026
9d58d31
fix(matrix): honour the shared send_read_receipts switch
iainlane Sep 29, 2026
e691062
test(matrix): raise the Synapse message limit only for boundary cases
iainlane Sep 29, 2026
458fbce
test(matrix): keep the default Synapse limit for the visibility cases
iainlane Sep 29, 2026
50bff0b
fix(gateway): translate the finished-turn /steer reply
iainlane Sep 29, 2026
5b00f2e
fix(gateway): give a priority-path steer one lifecycle
iainlane Sep 30, 2026
5417113
docs(gateway): describe how a timed-out turn completes
iainlane Sep 30, 2026
a0fd9ca
test(matrix): drop the unused parameter of the Synapse burst fixture
iainlane Sep 30, 2026
f290d04
fix(gateway): attach priority-path input only where follow-up hooks run
iainlane Sep 30, 2026
d78a8b2
fix(gateway): complete every leftover steer with the turn that reads it
iainlane Sep 30, 2026
a167a90
fix(gateway): let a leftover steer's own turn end its lifecycle
iainlane Sep 30, 2026
07063e8
fix(gateway): complete a handed-over leftover whose turn never starts
iainlane Sep 30, 2026
33145ae
test(matrix): bound the boundary cases' waits only against a hang
iainlane Sep 30, 2026
18ec219
fix(gateway): complete every started message once, including dropped …
iainlane Sep 30, 2026
aefe960
fix(gateway): complete a follow-up cancelled during its own start hook
iainlane Sep 30, 2026
10903b0
fix(gateway): hand a queued leftover steer's lifecycle to its copy
iainlane Sep 30, 2026
38cea2d
fix(gateway): complete a merged message with the turn that runs it
iainlane Sep 30, 2026
4792071
fix(gateway): complete parked messages when an adapter is torn down
iainlane Sep 30, 2026
7658fab
fix(gateway): complete a rescued orphan with the turn that runs it
iainlane Sep 30, 2026
da5c2de
test(gateway): drive /new and /reset through their busy handlers
iainlane Sep 30, 2026
01207aa
fix(gateway): attach a merged message only to a slot event with hooks
iainlane Sep 30, 2026
f0f307d
fix(gateway): complete parked messages after teardown cancels and flu…
iainlane Sep 30, 2026
addb707
fix(gateway): release the turn before completing a rescued orphan
iainlane Sep 30, 2026
3b4d8bd
fix(gateway): complete a started message that a busy-adapter merge drops
iainlane Sep 30, 2026
2d13897
fix(gateway): track started messages per adapter and complete them at…
iainlane Sep 30, 2026
c75b0d2
fix(gateway): complete messages merged into a follow-up without hooks…
iainlane Sep 30, 2026
9ca736b
fix(gateway): complete synthetic follow-up inputs after delivery
iainlane Oct 1, 2026
4e47b18
fix(gateway): connect receipt lifecycles to current input owners
iainlane Oct 2, 2026
a25e45e
fix(gateway): coalesce compatible media at recursion limits
iainlane Oct 2, 2026
99aa77a
test(matrix): compose native fixture configuration as YAML
iainlane Oct 2, 2026
722c788
fix(matrix): describe the read limit for members as well as events
iainlane Sep 30, 2026
6e8ac15
fix(matrix): describe pin order and read errors in matrix_read
iainlane Sep 30, 2026
a0f1963
feat(matrix): expose explicit pin actions in live sessions
iainlane Sep 27, 2026
df3f649
fix(matrix): report an unknown pin outcome when the sent write fails
iainlane Sep 30, 2026
fc6fe88
test(matrix): cover the refusal for an unknown requester power level
iainlane Sep 30, 2026
0e081c3
fix(matrix): check pin power after the other state reads
iainlane Oct 1, 2026
f876556
feat(matrix): administer rooms through opted-in live sessions
sanastasiou Sep 22, 2026
48da34c
fix(matrix): refuse administration when a DM becomes a group room
iainlane Sep 30, 2026
e2df9cd
fix(matrix): report an unknown outcome when a sent admin write fails
iainlane Sep 30, 2026
9fba6a7
fix(matrix): recheck requester power after admin preflight
iainlane Oct 1, 2026
94a5257
fix(matrix): report unknown admin outcomes for JSON response errors
iainlane Oct 1, 2026
778cca0
refactor(matrix): extract sync response and checkpoint lifecycle
iainlane Oct 2, 2026
e14b05a
feat(matrix): expose bot unread counts and scoped acknowledgements
iainlane Sep 28, 2026
02f7633
fix(matrix): report zero for threads missing from a synced room
iainlane Sep 29, 2026
8eb8674
fix(matrix): let hermes tools turn the unread toolset on and off
iainlane Sep 29, 2026
453c597
test(matrix): join a room again after leaving it and being re-invited
iainlane Sep 29, 2026
e1dfbf4
refactor(matrix): validate unread tool arguments only in the adapter
iainlane Sep 29, 2026
e231b04
refactor(matrix): verify receipt targets through client_events
iainlane Sep 30, 2026
1f1888c
test(matrix): bind the gateway loop in the unread tool tests
iainlane Sep 30, 2026
ad98ae3
test(matrix): resolve platform toolsets without the removed compat he…
iainlane Sep 30, 2026
9fed04b
docs(matrix): list the unread toolset in the toolsets reference
iainlane Sep 30, 2026
50d4148
fix(matrix): stop stale or interrupted explicit read writes
iainlane Oct 1, 2026
969c92b
test(matrix): compose room context with processing lifecycle
iainlane Oct 1, 2026
829626c
test(matrix): preserve scripted unread calls with receipt fixtures
iainlane Oct 1, 2026
102661d
fix(matrix): bound unread reads to the active owner request
iainlane Oct 1, 2026
fa402be
fix(matrix): bound requests when the gateway loop stops
iainlane Oct 1, 2026
1e072b0
test(matrix): select unread feedback through the shared fixture
iainlane Oct 2, 2026
ed1ae14
style: remove extra newline from source profile sibling
iainlane Oct 2, 2026
809b63c
fix: declare composed profile and RPC dependencies
iainlane Oct 2, 2026
12ace38
fix(gateway): complete rescued inputs when marker cleanup is cancelled
iainlane Oct 2, 2026
595e4e2
refactor(matrix): separate inbound dispatch and admission
iainlane Oct 2, 2026
de0344c
test(gateway): preserve stored context beside authored speech
iainlane Oct 4, 2026
2e26116
fix(gateway): preserve prepared input ownership in pending records
iainlane Oct 1, 2026
d8931e1
test(gateway): preserve stored context beside authored speech
iainlane Oct 4, 2026
aa779fa
refactor(gateway): extract platform notification and intake topics
iainlane Oct 1, 2026
ae609ca
test(gateway): exercise restored speech at the model boundary
iainlane Oct 5, 2026
b7e132d
fix(gateway): preserve leftover steer when pending admission fails
iainlane Oct 5, 2026
79e32e8
fix(gateway): restore accepted tracked steers at pending capacity
iainlane Oct 5, 2026
6ef9ccb
test(matrix): compose receipt fixtures with durable input handling
iainlane Oct 2, 2026
af6d4b4
fix(simplex): preserve native reply context
iainlane Oct 5, 2026
8172bb4
test(simplex): match current reply batching and prompt contracts
iainlane Oct 6, 2026
ce11f93
test: reconcile feedback fixtures with current typed owners
iainlane Oct 2, 2026
4c76944
fix(gateway): type marker handoff and validate dispatch handlers
iainlane Oct 6, 2026
f7c092a
fix(matrix): close real sessions in adapter test fixtures
iainlane Oct 9, 2026
b3d873a
refactor(agent): extract cached client teardown
iainlane Oct 9, 2026
dc28111
fix(tools): close persistent worker loops when threads exit
iainlane Oct 9, 2026
e7985a6
feat(matrix): inspect the current room through live reads
iainlane Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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'
68 changes: 1 addition & 67 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple, TYPE_CHECKING, Union
from urllib.parse import urlparse, parse_qs, urlunparse

from agent.auxiliary_client_lifecycle import _close_cached_client
from agent.error_classifier import (
_BILLING_PATTERNS,
_OVERLOADED_PATTERNS,
Expand Down Expand Up @@ -5899,73 +5900,6 @@ def neuter_async_httpx_del() -> None:
pass # Graceful degradation if the SDK changes its internals


def _force_close_async_httpx(client: Any) -> None:
"""Mark the httpx AsyncClient inside an AsyncOpenAI client as closed so ``__del__`` won't
schedule ``aclose()`` on a dead loop. Skips the full async close — the OS drops connections."""
with contextlib.suppress(Exception):
from httpx._client import ClientState
inner = getattr(client, "_client", None)
if inner is not None and not getattr(inner, "is_closed", True):
inner._state = ClientState.CLOSED


def _schedule_async_close(close_result: Any, client: Any) -> None:
"""Finish an async close without leaking an unawaited coroutine."""
async def _await_close() -> None:
try:
await close_result
except Exception:
pass
finally:
_force_close_async_httpx(client)
runner = _await_close()
try:
import asyncio as _aio
try:
loop = _aio.get_running_loop()
except RuntimeError:
_aio.run(runner)
else:
task = loop.create_task(runner)

def _consume(completed_task) -> None:
with contextlib.suppress(BaseException):
completed_task.exception()
task.add_done_callback(_consume)
runner = None
except Exception:
if runner is not None:
with contextlib.suppress(Exception):
runner.close()
_force_close_async_httpx(client)


def _close_cached_client(client: Any, *, close_async: bool = False) -> None:
"""Close one cached client, awaiting async transports only when safe."""
if client is None:
return
close_fn = getattr(client, "close", None)
if not callable(close_fn):
_force_close_async_httpx(client)
return
try:
close_result = close_fn()
except Exception:
_force_close_async_httpx(client)
return
if inspect.isawaitable(close_result):
if close_async:
_schedule_async_close(close_result, client)
else:
# Never await a client owned by another live loop; close the coroutine (no
# unawaited warning) and neuter the transport.
with contextlib.suppress(Exception):
close_result.close()
_force_close_async_httpx(client)
return
_force_close_async_httpx(client)


def shutdown_cached_clients() -> None:
"""Close all cached clients; call at CLI shutdown *before* the loop closes.

Expand Down
103 changes: 103 additions & 0 deletions agent/auxiliary_client_lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Teardown of cached auxiliary-client transports."""

import contextlib
import inspect
from typing import Any, Optional, TYPE_CHECKING

if TYPE_CHECKING:
from asyncio import Task


def _force_close_async_httpx(client: Any) -> None:
"""Mark the httpx AsyncClient inside an AsyncOpenAI client as closed so ``__del__`` won't
schedule ``aclose()`` on a dead loop. Skips the full async close — the OS drops connections."""
with contextlib.suppress(Exception):
from httpx._client import ClientState

inner = getattr(client, "_client", None)
if inner is not None and not getattr(inner, "is_closed", True):
inner._state = ClientState.CLOSED


def _schedule_async_close(close_result: Any, client: Any) -> Optional["Task[None]"]:
"""Finish an async close without leaking an unawaited coroutine."""

async def _await_close() -> None:
try:
await close_result
except Exception:
pass
finally:
_force_close_async_httpx(client)

runner = _await_close()
try:
import asyncio as _aio

try:
loop = _aio.get_running_loop()
except RuntimeError:
_aio.run(runner)
else:
task = loop.create_task(runner)

def _consume(completed_task) -> None:
with contextlib.suppress(BaseException):
completed_task.exception()

task.add_done_callback(_consume)
runner = None
return task
except Exception:
if runner is not None:
with contextlib.suppress(Exception):
runner.close()
_force_close_async_httpx(client)


def _close_cached_client(
client: Any, *, close_async: bool = False
) -> Optional["Task[None]"]:
"""Close one cached client, awaiting async transports only when safe."""
if client is None:
return
close_fn = getattr(client, "close", None)
if not callable(close_fn):
_force_close_async_httpx(client)
return
try:
close_result = close_fn()
except Exception:
_force_close_async_httpx(client)
return
if inspect.isawaitable(close_result):
if close_async:
return _schedule_async_close(close_result, client)
else:
# Never await a client owned by another live loop; close the coroutine (no
# unawaited warning) and neuter the transport.
with contextlib.suppress(Exception):
close_result.close()
_force_close_async_httpx(client)
return
_force_close_async_httpx(client)


async def shutdown_cached_clients_for_current_loop() -> None:
"""Close cached transports owned by the current event loop."""
import asyncio
from agent import auxiliary_client

loop = asyncio.get_running_loop()
with auxiliary_client._client_cache_lock:
keys = [
key
for key, entry in auxiliary_client._client_cache.items()
if entry[2] is loop
]
clients = [auxiliary_client._client_cache.pop(key)[0] for key in keys]

tasks = [_close_cached_client(client, close_async=True) for client in clients]
pending = [task for task in tasks if task is not None]
if pending:
await asyncio.gather(*pending)
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
Loading