Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
34e60b4
fix(matrix): keep source links on the current turn
iainlane Sep 27, 2026
0f8bec9
test(matrix): verify source link through a separate client
iainlane Sep 27, 2026
e5c6d96
fix(matrix): add a via server for domainless room IDs
iainlane Sep 27, 2026
f9644dd
fix(matrix): route source links through the joined bot server
iainlane Sep 27, 2026
6878dd3
fix(matrix): link the triggering event, not the thread root
iainlane Sep 29, 2026
c2758d1
fix(matrix): choose source-link via servers from room state
iainlane Sep 29, 2026
d364fdc
test(matrix): drop prompt tests with no reachable output
iainlane Sep 29, 2026
ba640d2
test(matrix): seed a real state store in the context and voice tests
iainlane Sep 29, 2026
7b67706
fix(matrix): describe the read limit for members as well as events
iainlane Sep 30, 2026
21aff2d
fix(matrix): describe pin order and read errors in matrix_read
iainlane Sep 30, 2026
b90ff14
fix(matrix): restrict private bot chats to two joined users
iainlane Sep 27, 2026
28c04e9
test(matrix): give DM fixtures complete joined membership
iainlane Sep 27, 2026
b226580
fix(matrix): adopt the homeserver's user ID after password login
iainlane Sep 29, 2026
e67d37f
test(matrix): give the handoff fixture complete joined membership
iainlane Sep 29, 2026
b14dfc1
docs(matrix): describe the two-joined-users rule in the env reference
iainlane Sep 29, 2026
931f5ab
fix(matrix): keep the E2EE store keyed by the configured user ID
iainlane Sep 29, 2026
b18c442
feat(matrix): name unnamed rooms after their members
iainlane Sep 27, 2026
3e643db
feat(matrix): surface room state changes to the agent
iainlane Sep 27, 2026
5a5a1ad
fix(matrix): attach room notes when preparing model input
iainlane Sep 27, 2026
63d0295
fix(matrix): pin initial room metadata in conversation prompts
iainlane Sep 27, 2026
59d67ba
fix(matrix): refresh room origin on reset and fan out state notes
iainlane Sep 27, 2026
566fc34
fix(matrix): reconcile room state after gateway restart
iainlane Sep 27, 2026
a50000b
fix(matrix): acknowledge room state with the transcript turn
iainlane Sep 27, 2026
985b105
fix(matrix): keep one history backfill when text batches merge
iainlane Sep 29, 2026
554417d
fix(matrix): report room changes through a generic adapter turn hook
iainlane Sep 29, 2026
357080c
fix(matrix): keep the room baseline when state reads fail
iainlane Sep 29, 2026
b16f279
fix(matrix): acknowledge room security changes with the saved turn
iainlane Sep 29, 2026
03269ff
test(matrix): expect /new to pass the chat source to reset_session
iainlane Sep 29, 2026
1691f5e
fix(matrix): keep origin names when an internal wake exhausts compres…
iainlane Sep 29, 2026
9a25a18
fix(matrix): look up the turn-context session off the event loop
iainlane Sep 29, 2026
0bc6ece
refactor(matrix): derive the frozen session prompt from the turn hook
iainlane Sep 29, 2026
440b639
fix(matrix): keep the last room names when a state read fails
iainlane Sep 29, 2026
d93faec
fix(matrix): keep the room baseline across context compaction
iainlane Sep 30, 2026
c912650
test(matrix): cover the channel state on a summary merged into the tail
iainlane Sep 30, 2026
6467306
test(matrix): exercise live gateway with separate client
iainlane Sep 27, 2026
c681a4e
fix(matrix): clean up live test resources after worker exit
iainlane Sep 27, 2026
6e04b30
fix(matrix): isolate live gateway image builds
iainlane Sep 27, 2026
3c44823
fix(matrix): reap interrupted live gateway images
iainlane Sep 27, 2026
2c0dfc5
test(matrix): use a cached gateway image for live tests
iainlane Sep 27, 2026
2a9eb0e
test(matrix): bound plain exchange wait
iainlane Sep 27, 2026
0c9db03
test(matrix): set finite event rates for disposable Synapse
iainlane Sep 28, 2026
1fab3ce
test(matrix): keep the selected Docker context for live tests
iainlane Sep 29, 2026
6334adf
test(matrix): drop the gateway image's crypto probe container
iainlane Sep 29, 2026
0a6e265
ci(matrix): build the live gateway image before the tests
iainlane Sep 29, 2026
627d54b
test(matrix): run the live gateway as the host user
iainlane Sep 29, 2026
d4a9d14
test(matrix): listen for the live gateway only on its route to this host
iainlane Sep 29, 2026
87bf4fc
test(matrix): wait for the live gateway to finish startup restore
iainlane Sep 29, 2026
f22ae86
test(matrix): fix the Windows gateway user and widen the live CI filter
iainlane Sep 29, 2026
5669934
fix(matrix): keep a user's leading quote in an explicit reply
iainlane Sep 29, 2026
e75b5a9
fix(matrix): read the author from an emote reply fallback
iainlane Sep 30, 2026
1d29b7f
fix(telegram): preserve reply context across message batches
the3asic Jul 16, 2026
d661176
fix(gateway): keep reply context when busy follow-ups merge
iainlane Sep 30, 2026
9bc6fcc
fix(gateway): preserve inline flags in busy text merges
iainlane Oct 1, 2026
ea117f0
fix(matrix): distinguish thread fallback from explicit replies
iainlane Sep 27, 2026
48bf705
feat(matrix): resolve quoted messages from room events
iainlane Sep 27, 2026
508c130
feat(matrix): restore quoted media and new thread context
iainlane Sep 27, 2026
465869d
test(matrix): cover media reply failure and separate thread context
iainlane Sep 27, 2026
cb0d2e0
fix(matrix): preserve thread reply context across event changes
iainlane Sep 27, 2026
e596e35
test(matrix): verify thread context with a separate client
iainlane Sep 27, 2026
ae7f18e
fix(matrix): exclude redacted context from in-flight reads
iainlane Sep 27, 2026
a93a19d
fix(matrix): keep reply pointers unattributed and quotes whole
iainlane Sep 29, 2026
1e9a0f1
fix(matrix): strip only a real reply fallback from thread messages
iainlane Sep 29, 2026
609812d
fix(matrix): ignore edits whose target is not cached
iainlane Sep 29, 2026
1ad396b
fix(matrix): keep reply context when a busy session merges a message
iainlane Sep 29, 2026
1595549
fix(matrix): leave every chunk of a batched turn out of thread backfill
iainlane Sep 29, 2026
51f091d
test(matrix): cover decryption of reply targets and thread events
iainlane Sep 29, 2026
75d3e99
fix(matrix): fetch thread history through the turn-context hook
iainlane Sep 29, 2026
b48bab1
fix(matrix): use the reply-fallback check for thread messages
iainlane Sep 30, 2026
5b326c6
fix(matrix): share one <mx-reply> check between reply helpers
iainlane Sep 30, 2026
d8c6a58
fix(matrix): exclude every busy-debounced trigger from thread history
iainlane Oct 1, 2026
74d5b38
fix(matrix): append thread fields after existing event fields
iainlane Oct 1, 2026
120bf0c
fix(matrix): preserve user quotations in resolved context
iainlane Oct 1, 2026
bc4e5b0
fix(matrix): exclude batched thread roots from backfill
iainlane Oct 1, 2026
2eefeb2
docs(matrix): describe reply fallback and merged event identities
iainlane Oct 1, 2026
ebfd461
test(matrix): expose incorrect thread fallback chains
iainlane Sep 27, 2026
acf96c8
fix(matrix): preserve genuine thread reply semantics
mijanx Jun 22, 2026
ad2f26b
fix(matrix): chain thread fallbacks through known events
iainlane Sep 27, 2026
010cb02
test(matrix): prove genuine thread replies with a live client
iainlane Sep 27, 2026
e8c993d
fix(matrix): quote the triggering event once per threaded response
iainlane Sep 29, 2026
c014402
refactor(matrix): drop the unused thread fallback alias
iainlane Sep 29, 2026
2e49b0c
fix(matrix): remember recent answered and sent events per thread
iainlane Sep 29, 2026
a6d44d5
feat(matrix): read bounded history through the live session
iainlane Sep 27, 2026
b488180
fix(matrix): keep live reads on the gateway event loop
iainlane Sep 27, 2026
a687925
refactor(matrix): share history decoding with the thread backfill
iainlane Sep 29, 2026
8ed8bb9
refactor(matrix): bind the gateway loop for Matrix reads explicitly
iainlane Sep 29, 2026
f92cfa8
fix(matrix): refuse Matrix reads through a non-Matrix transport
iainlane Sep 29, 2026
a2e365a
fix(matrix): list Matrix reads oldest first
iainlane Sep 29, 2026
70341bb
fix(matrix): count only messages towards the Matrix read limit
iainlane Sep 29, 2026
512824f
test(matrix): cover read policy, argument bounds and the deadline
iainlane Sep 29, 2026
23066c1
feat(matrix): configure the Matrix read toolset through hermes tools
iainlane Sep 29, 2026
08b2374
test(matrix): wait for the final reply in the live read-tools test
iainlane Sep 30, 2026
3a39b39
test(matrix): start the read deadline only once the stalled read runs
iainlane Sep 30, 2026
c306334
fix(matrix): require a target for thread and event reads
iainlane Oct 1, 2026
97dfa01
refactor(matrix): decode encrypted history with its concrete event type
iainlane Oct 1, 2026
ceb7cc9
feat(matrix): backfill room and thread context on admitted mentions
andyylin Sep 27, 2026
a06e751
docs(matrix): describe catch-up in bot threads and new thread sessions
iainlane Sep 29, 2026
a04382c
fix(matrix): list the room backfill limit among the config defaults
iainlane Sep 29, 2026
112b8bf
fix(matrix): pass over the bot's status notices in mention catch-up
iainlane Sep 29, 2026
17851e3
docs(matrix): explain where catch-up stops after a session reset
iainlane Sep 29, 2026
ce806c6
fix(matrix): mark the shutdown notices as status notices
iainlane Sep 30, 2026
5fe7066
refactor(matrix): let platforms declare that they read the status mark
iainlane Sep 30, 2026
68ad368
docs(matrix): limit the /new catch-up rule to the main timeline
iainlane Sep 30, 2026
9513857
test(matrix): use event boundaries in quote-preservation fixtures
iainlane Oct 1, 2026
8407bec
fix(matrix): exclude current batched events from mention catch-up
iainlane Oct 1, 2026
7137600
refactor(matrix): share the bounded history decoder between read layers
iainlane Oct 1, 2026
1bf6e97
feat(matrix): expose canonical matrix.to source permalink in session …
pretyflaco Sep 22, 2026
48666fb
fix(matrix): report undecryptable reactions against their target
iainlane Sep 29, 2026
77e3e85
test(matrix): cover reaction truncation, decryption notices and passi…
iainlane Sep 29, 2026
5f8344a
test(matrix): expect the skip count in reaction reads
iainlane Sep 29, 2026
a2e0810
refactor(matrix): decrypt history and reactions through one helper
iainlane Sep 29, 2026
50dc887
test(matrix): tie catch-up reactions to the messages that the scan kept
iainlane Sep 29, 2026
82e3bd1
test(matrix): leave status notices out of catch-up reaction lookups
iainlane Sep 30, 2026
eb16404
fix(matrix): integrate reaction lookups with current history readers
iainlane Sep 30, 2026
4fde799
test(matrix): adapt reaction fixtures to the shared SDK decoder
iainlane Oct 1, 2026
0a0d86f
test(matrix): use the new reply-context signature in effective-state …
iainlane Sep 30, 2026
3e8095d
test(matrix): make the native context hand-offs deterministic
iainlane Sep 30, 2026
720c1dc
fix(matrix): let a redacted event not end mention catch-up
iainlane Sep 30, 2026
8387e53
refactor(matrix): remove the string-returning history methods
iainlane Sep 30, 2026
32d718f
fix(matrix): give media events without a file empty attachment lists
iainlane Sep 30, 2026
ea5f0cd
fix(matrix): quote the routed profile's copy of a cached image
iainlane Sep 30, 2026
d8a02db
fix(matrix): preserve repaired readers in effective-event integration
iainlane Oct 1, 2026
590d16c
refactor(gateway): isolate inbound media profile routing
iainlane Oct 1, 2026
bcdf93b
fix(matrix): retain shared quoted files during profile routing
iainlane Oct 1, 2026
725b7a3
fix(gateway): omit attachments that could not enter the routed cache
iainlane Oct 1, 2026
6e2d04e
feat(matrix): discover joined rooms, Spaces and directory users
iainlane Sep 28, 2026
1fd8c56
test(matrix): wait for the user directory before live discovery
iainlane Sep 30, 2026
3980fd5
test(matrix): let one slow homeserver request finish in discovery set-up
iainlane Sep 30, 2026
0ce0278
test(matrix): wait for the homeserver directory rebuild
iainlane Oct 1, 2026
7912c64
test(relay): complete recording transport protocol
iainlane Oct 5, 2026
0b62f92
test(matrix): serialize assigned URLs in encrypted-file fixtures
iainlane Oct 6, 2026
7408523
feat(matrix): show reactions in reads and catch-up
iainlane Sep 27, 2026
0fea0bc
test(matrix): compose source notes with reaction backfill
iainlane Oct 6, 2026
a657242
test(matrix): bound retries in the live fixture clients
iainlane Oct 7, 2026
3d4e9fa
test(matrix): bound retries in the live fixture clients
iainlane Oct 7, 2026
2981fed
feat(matrix): inspect the current room through live reads
iainlane Sep 27, 2026
422dd09
fix(matrix): close real sessions in adapter test fixtures
iainlane Oct 9, 2026
c865a67
fix(matrix): keep effective event state through model input preparation
iainlane Sep 27, 2026
59b8939
feat(matrix): create threads through the live session
iainlane Sep 28, 2026
65015ff
docs(matrix): list the Matrix Threads toolset beside Matrix History
iainlane Sep 29, 2026
cec19c5
fix(matrix): record a delivered thread message before the ownership c…
iainlane Sep 29, 2026
ce7b501
refactor(matrix): schedule thread creation like the read tool
iainlane Sep 29, 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
22 changes: 19 additions & 3 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,
) -> tuple[list[dict[str, Any]], list[str]]:
*,
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
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