Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
d202d49
fix(matrix): keep source links on the current turn
iainlane Sep 27, 2026
f57a737
test(matrix): verify source link through a separate client
iainlane Sep 27, 2026
e2b3f9b
fix(matrix): add a via server for domainless room IDs
iainlane Sep 27, 2026
d90fb77
fix(matrix): route source links through the joined bot server
iainlane Sep 27, 2026
5b6ff9d
fix(matrix): link the triggering event, not the thread root
iainlane Sep 29, 2026
a223557
fix(matrix): choose source-link via servers from room state
iainlane Sep 29, 2026
735cea9
test(matrix): drop prompt tests with no reachable output
iainlane Sep 29, 2026
8c3bf5c
test(matrix): seed a real state store in the context and voice tests
iainlane Sep 29, 2026
5926a6a
fix(matrix): describe the read limit for members as well as events
iainlane Sep 30, 2026
ca2f550
fix(matrix): describe pin order and read errors in matrix_read
iainlane Sep 30, 2026
73b0b4c
fix(matrix): restrict private bot chats to two joined users
iainlane Sep 27, 2026
051287e
test(matrix): give DM fixtures complete joined membership
iainlane Sep 27, 2026
aa5affe
fix(matrix): adopt the homeserver's user ID after password login
iainlane Sep 29, 2026
dc66848
test(matrix): give the handoff fixture complete joined membership
iainlane Sep 29, 2026
ee68125
docs(matrix): describe the two-joined-users rule in the env reference
iainlane Sep 29, 2026
56aca9c
fix(matrix): keep the E2EE store keyed by the configured user ID
iainlane Sep 29, 2026
01d34b9
feat(matrix): name unnamed rooms after their members
iainlane Sep 27, 2026
33db344
feat(matrix): surface room state changes to the agent
iainlane Sep 27, 2026
c107bd9
fix(matrix): attach room notes when preparing model input
iainlane Sep 27, 2026
8486463
fix(matrix): pin initial room metadata in conversation prompts
iainlane Sep 27, 2026
8f52d2d
fix(matrix): refresh room origin on reset and fan out state notes
iainlane Sep 27, 2026
8787bdd
fix(matrix): reconcile room state after gateway restart
iainlane Sep 27, 2026
976e49d
fix(matrix): acknowledge room state with the transcript turn
iainlane Sep 27, 2026
a43d44c
fix(matrix): keep one history backfill when text batches merge
iainlane Sep 29, 2026
1df2933
fix(matrix): report room changes through a generic adapter turn hook
iainlane Sep 29, 2026
5eb475b
fix(matrix): keep the room baseline when state reads fail
iainlane Sep 29, 2026
188d829
fix(matrix): acknowledge room security changes with the saved turn
iainlane Sep 29, 2026
f014df2
test(matrix): expect /new to pass the chat source to reset_session
iainlane Sep 29, 2026
0f854cd
fix(matrix): keep origin names when an internal wake exhausts compres…
iainlane Sep 29, 2026
8589f6d
fix(matrix): look up the turn-context session off the event loop
iainlane Sep 29, 2026
da99d38
refactor(matrix): derive the frozen session prompt from the turn hook
iainlane Sep 29, 2026
6d40081
fix(matrix): keep the last room names when a state read fails
iainlane Sep 29, 2026
8ff8e30
fix(matrix): keep the room baseline across context compaction
iainlane Sep 30, 2026
5b6b6d8
test(matrix): cover the channel state on a summary merged into the tail
iainlane Sep 30, 2026
3196d63
test(matrix): exercise live gateway with separate client
iainlane Sep 27, 2026
98e724e
fix(matrix): clean up live test resources after worker exit
iainlane Sep 27, 2026
4453942
fix(matrix): isolate live gateway image builds
iainlane Sep 27, 2026
64f6a7b
fix(matrix): reap interrupted live gateway images
iainlane Sep 27, 2026
34f967c
test(matrix): use a cached gateway image for live tests
iainlane Sep 27, 2026
fda5cbc
test(matrix): bound plain exchange wait
iainlane Sep 27, 2026
819be40
test(matrix): set finite event rates for disposable Synapse
iainlane Sep 28, 2026
57c5271
test(matrix): keep the selected Docker context for live tests
iainlane Sep 29, 2026
c7efc42
test(matrix): drop the gateway image's crypto probe container
iainlane Sep 29, 2026
afbd20b
ci(matrix): build the live gateway image before the tests
iainlane Sep 29, 2026
d3a6c3f
test(matrix): run the live gateway as the host user
iainlane Sep 29, 2026
17245d3
test(matrix): listen for the live gateway only on its route to this host
iainlane Sep 29, 2026
7e444de
test(matrix): wait for the live gateway to finish startup restore
iainlane Sep 29, 2026
68bda3e
test(matrix): fix the Windows gateway user and widen the live CI filter
iainlane Sep 29, 2026
0c9947f
fix(matrix): keep a user's leading quote in an explicit reply
iainlane Sep 29, 2026
3afdc40
fix(matrix): read the author from an emote reply fallback
iainlane Sep 30, 2026
c168a2d
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
b7fb39a
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
74809b5
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
941a763
fix(matrix): distinguish thread fallback from explicit replies
iainlane Sep 27, 2026
7afc5f4
feat(matrix): resolve quoted messages from room events
iainlane Sep 27, 2026
e872f62
feat(matrix): restore quoted media and new thread context
iainlane Sep 27, 2026
7dd027e
test(matrix): cover media reply failure and separate thread context
iainlane Sep 27, 2026
cba4335
fix(matrix): preserve thread reply context across event changes
iainlane Sep 27, 2026
3ae013b
test(matrix): verify thread context with a separate client
iainlane Sep 27, 2026
d1c30eb
fix(matrix): exclude redacted context from in-flight reads
iainlane Sep 27, 2026
57fb938
fix(matrix): keep reply pointers unattributed and quotes whole
iainlane Sep 29, 2026
23668a5
fix(matrix): strip only a real reply fallback from thread messages
iainlane Sep 29, 2026
52b540b
fix(matrix): ignore edits whose target is not cached
iainlane Sep 29, 2026
bbd9d96
fix(matrix): keep reply context when a busy session merges a message
iainlane Sep 29, 2026
09278ad
fix(matrix): leave every chunk of a batched turn out of thread backfill
iainlane Sep 29, 2026
e80d7f8
test(matrix): cover decryption of reply targets and thread events
iainlane Sep 29, 2026
8ceb82a
fix(matrix): fetch thread history through the turn-context hook
iainlane Sep 29, 2026
0caac84
fix(matrix): use the reply-fallback check for thread messages
iainlane Sep 30, 2026
4fa3e79
fix(matrix): share one <mx-reply> check between reply helpers
iainlane Sep 30, 2026
7fbd315
fix(matrix): exclude every busy-debounced trigger from thread history
iainlane Oct 1, 2026
2d895c9
fix(matrix): append thread fields after existing event fields
iainlane Oct 1, 2026
350444f
fix(matrix): preserve user quotations in resolved context
iainlane Oct 1, 2026
edb226e
fix(matrix): exclude batched thread roots from backfill
iainlane Oct 1, 2026
780481e
docs(matrix): describe reply fallback and merged event identities
iainlane Oct 1, 2026
73e2971
test(matrix): expose incorrect thread fallback chains
iainlane Sep 27, 2026
087e481
fix(matrix): preserve genuine thread reply semantics
mijanx Jun 22, 2026
096aef1
fix(matrix): chain thread fallbacks through known events
iainlane Sep 27, 2026
a24798d
test(matrix): prove genuine thread replies with a live client
iainlane Sep 27, 2026
d4467f2
fix(matrix): quote the triggering event once per threaded response
iainlane Sep 29, 2026
8c78610
refactor(matrix): drop the unused thread fallback alias
iainlane Sep 29, 2026
650f693
fix(matrix): remember recent answered and sent events per thread
iainlane Sep 29, 2026
879219e
feat(matrix): read bounded history through the live session
iainlane Sep 27, 2026
ec59a44
fix(matrix): keep live reads on the gateway event loop
iainlane Sep 27, 2026
c073100
refactor(matrix): share history decoding with the thread backfill
iainlane Sep 29, 2026
417f6c8
refactor(matrix): bind the gateway loop for Matrix reads explicitly
iainlane Sep 29, 2026
dbd7d26
fix(matrix): refuse Matrix reads through a non-Matrix transport
iainlane Sep 29, 2026
974f4ab
fix(matrix): list Matrix reads oldest first
iainlane Sep 29, 2026
e3b2fa7
fix(matrix): count only messages towards the Matrix read limit
iainlane Sep 29, 2026
059c6ed
test(matrix): cover read policy, argument bounds and the deadline
iainlane Sep 29, 2026
af6c4f0
feat(matrix): configure the Matrix read toolset through hermes tools
iainlane Sep 29, 2026
ede8688
test(matrix): wait for the final reply in the live read-tools test
iainlane Sep 30, 2026
3eca3e0
test(matrix): start the read deadline only once the stalled read runs
iainlane Sep 30, 2026
1e6a065
fix(matrix): require a target for thread and event reads
iainlane Oct 1, 2026
b1aaddf
refactor(matrix): decode encrypted history with its concrete event type
iainlane Oct 1, 2026
01cf772
feat(matrix): backfill room and thread context on admitted mentions
andyylin Sep 27, 2026
0691566
docs(matrix): describe catch-up in bot threads and new thread sessions
iainlane Sep 29, 2026
db7f64f
fix(matrix): list the room backfill limit among the config defaults
iainlane Sep 29, 2026
252a119
fix(matrix): pass over the bot's status notices in mention catch-up
iainlane Sep 29, 2026
6b52ef3
docs(matrix): explain where catch-up stops after a session reset
iainlane Sep 29, 2026
c3eeffa
fix(matrix): mark the shutdown notices as status notices
iainlane Sep 30, 2026
f18f543
refactor(matrix): let platforms declare that they read the status mark
iainlane Sep 30, 2026
904c1f3
docs(matrix): limit the /new catch-up rule to the main timeline
iainlane Sep 30, 2026
953d5a7
test(matrix): use event boundaries in quote-preservation fixtures
iainlane Oct 1, 2026
2111a1f
fix(matrix): exclude current batched events from mention catch-up
iainlane Oct 1, 2026
b5d48f8
refactor(matrix): share the bounded history decoder between read layers
iainlane Oct 1, 2026
0aa9bcd
feat(matrix): expose canonical matrix.to source permalink in session …
pretyflaco Sep 22, 2026
bf45f21
test(matrix): prove captions and filenames through live media
iainlane Sep 27, 2026
277e271
fix(matrix): classify media captions from the wire body
iainlane Sep 27, 2026
01c5057
test(matrix): bound media caption reply wait
iainlane Sep 27, 2026
24f5155
fix(matrix): ignore the reply fallback when classifying media captions
iainlane Sep 29, 2026
22264d8
test(matrix): drop a media test that passes without the fix
iainlane Sep 29, 2026
3c9f531
test(matrix): use a live caption that the legacy heuristic drops
iainlane Sep 29, 2026
3b6f0c2
refactor(matrix): extract inbound media filename classification
iainlane Oct 1, 2026
5d12c41
fix(matrix): preserve authored quotations in media captions
iainlane Oct 1, 2026
5a92afb
style(matrix): separate media classifiers from filename constants
iainlane Oct 1, 2026
2725bb8
fix(matrix): surface oversized inbound media
ooiuuii Jul 26, 2026
299bd59
fix(matrix): cap inbound media by its downloaded length
iainlane Sep 29, 2026
2a3654c
test(matrix): import json for the oversized live media test
iainlane Sep 29, 2026
5e4dd78
fix(matrix): keep the filename in a too-large marker for a reply
iainlane Sep 29, 2026
9c68401
test(matrix): check the media endpoint and token for downloads
iainlane Sep 29, 2026
fc44d59
fix(matrix): apply media download limits to quoted images
iainlane Oct 1, 2026
ae7a08e
test(matrix): configure the native media cap through gateway settings
iainlane Oct 1, 2026
dee3d43
fix(matrix): keep oversized markers compatible with attachment proces…
iainlane Oct 1, 2026
c887e75
fix(matrix): report undecryptable reactions against their target
iainlane Sep 29, 2026
777a562
test(matrix): cover reaction truncation, decryption notices and passi…
iainlane Sep 29, 2026
bf12df4
test(matrix): expect the skip count in reaction reads
iainlane Sep 29, 2026
0ad33c2
refactor(matrix): decrypt history and reactions through one helper
iainlane Sep 29, 2026
58a4603
test(matrix): tie catch-up reactions to the messages that the scan kept
iainlane Sep 29, 2026
d88f50d
test(matrix): leave status notices out of catch-up reaction lookups
iainlane Sep 30, 2026
e95d532
fix(matrix): integrate reaction lookups with current history readers
iainlane Sep 30, 2026
6f99dda
test(matrix): adapt reaction fixtures to the shared SDK decoder
iainlane Oct 1, 2026
dc6d06a
fix(matrix): keep effective event state through model input preparation
iainlane Sep 27, 2026
59612c4
test(matrix): use the new reply-context signature in effective-state …
iainlane Sep 30, 2026
72f06f6
test(matrix): make the native context hand-offs deterministic
iainlane Sep 30, 2026
6c08e20
fix(matrix): let a redacted event not end mention catch-up
iainlane Sep 30, 2026
f56b64a
refactor(matrix): remove the string-returning history methods
iainlane Sep 30, 2026
82c6823
fix(matrix): give media events without a file empty attachment lists
iainlane Sep 30, 2026
24ac038
fix(matrix): quote the routed profile's copy of a cached image
iainlane Sep 30, 2026
e6913c2
fix(matrix): preserve repaired readers in effective-event integration
iainlane Oct 1, 2026
ba3de23
refactor(gateway): isolate inbound media profile routing
iainlane Oct 1, 2026
0c28497
fix(matrix): retain shared quoted files during profile routing
iainlane Oct 1, 2026
d2d33fa
fix(gateway): omit attachments that could not enter the routed cache
iainlane Oct 1, 2026
52438ba
feat(matrix): expose explicit pin actions in live sessions
iainlane Sep 27, 2026
2d9d233
fix(matrix): report an unknown pin outcome when the sent write fails
iainlane Sep 30, 2026
50ceb01
test(matrix): cover the refusal for an unknown requester power level
iainlane Sep 30, 2026
99bd248
fix(matrix): check pin power after the other state reads
iainlane Oct 1, 2026
72c0e90
feat(matrix): administer rooms through opted-in live sessions
sanastasiou Sep 22, 2026
ea5df07
fix(matrix): refuse administration when a DM becomes a group room
iainlane Sep 30, 2026
1b1c3e0
fix(matrix): report an unknown outcome when a sent admin write fails
iainlane Sep 30, 2026
2575cab
fix(matrix): recheck requester power after admin preflight
iainlane Oct 1, 2026
b373364
fix(matrix): report unknown admin outcomes for JSON response errors
iainlane Oct 1, 2026
9638f98
fix(matrix): honor MSC2530 filename field for inbound media captions
CocaKova Sep 27, 2026
38557dc
test(matrix): classify the pin room with the current owner API
iainlane Oct 2, 2026
ccafa08
test(relay): complete recording transport protocol
iainlane Oct 5, 2026
88e2f06
test(matrix): serialize assigned URLs in encrypted-file fixtures
iainlane Oct 6, 2026
37f42f1
feat(matrix): show reactions in reads and catch-up
iainlane Sep 27, 2026
e0ea2c6
test(matrix): compose source notes with reaction backfill
iainlane Oct 6, 2026
f13f5d0
test(matrix): compose feature contracts with current native owners
iainlane Oct 2, 2026
7a4d39e
test(matrix): bound retries in the live fixture clients
iainlane Oct 7, 2026
fbb26d9
feat(matrix): inspect the current room through live reads
iainlane Sep 27, 2026
46c24f0
test(matrix): model missing SDK packages in rebind fixtures
iainlane Oct 7, 2026
cbc39df
fix(matrix): close real sessions in adapter test fixtures
iainlane Oct 9, 2026
9859260
fix(matrix): bound requests when the gateway loop stops
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
122 changes: 122 additions & 0 deletions gateway/inbound_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""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

_T = TypeVar("_T")

_LOOP_CALL_TIMEOUT_SECONDS = 10.0


class InboundContextSnapshot(Protocol):
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]: ...


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


@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[QuotedImageEnrichment, ...] = ()
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
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 authored
]

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.text
current = self.snapshot.reply_image_paths()
descriptions = [
image.text
for image in self.quoted_images
if image.path in current 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
2 changes: 2 additions & 0 deletions gateway/platform_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class PlatformEntry:
allow_all_env: str = "" # truthy "allow everyone" switch
max_message_length: int = 0 # smart-chunking cap; 0 = no limit
pii_safe: bool = False # session descriptions redact PII (phone numbers, etc.)
# The adapter reads ``metadata["non_conversational"]``, so the gateway sets it on status sends.
reads_non_conversational_mark: bool = False
emoji: str = "🔌" # CLI/gateway display
allow_update_command: bool = True # /update may be issued from this platform
platform_hint: str = "" # injected into the system prompt; empty = none
Expand Down
Loading