Merge pull request #2156 from NousResearch/hermes/hermes-6757a563

fix(signal): handle Note to Self messages with echo-back protection
This commit is contained in:
Teknium
2026-03-20 04:56:57 -07:00
committed by GitHub
2 changed files with 50 additions and 5 deletions

View File

@@ -179,6 +179,11 @@ class SignalAdapter(BasePlatformAdapter):
# Normalize account for self-message filtering
self._account_normalized = self.account.strip()
# Track recently sent message timestamps to prevent echo-back loops
# in Note to Self / self-chat mode (mirrors WhatsApp recentlySentIds)
self._recent_sent_timestamps: set = set()
self._max_recent_timestamps = 50
logger.info("Signal adapter initialized: url=%s account=%s groups=%s",
self.http_url, _redact_phone(self.account),
"enabled" if self.group_allow_from else "disabled")
@@ -353,10 +358,26 @@ class SignalAdapter(BasePlatformAdapter):
# Unwrap nested envelope if present
envelope_data = envelope.get("envelope", envelope)
# Filter syncMessage envelopes (sent transcripts, read receipts, etc.)
# signal-cli may set syncMessage to null vs omitting it, so check key existence
# Handle syncMessage: extract "Note to Self" messages (sent to own account)
# while still filtering other sync events (read receipts, typing, etc.)
is_note_to_self = False
if "syncMessage" in envelope_data:
return
sync_msg = envelope_data.get("syncMessage")
if sync_msg and isinstance(sync_msg, dict):
sent_msg = sync_msg.get("sentMessage")
if sent_msg and isinstance(sent_msg, dict):
dest = sent_msg.get("destinationNumber") or sent_msg.get("destination")
sent_ts = sent_msg.get("timestamp")
if dest == self._account_normalized:
# Check if this is an echo of our own outbound reply
if sent_ts and sent_ts in self._recent_sent_timestamps:
self._recent_sent_timestamps.discard(sent_ts)
return
# Genuine user Note to Self — promote to dataMessage
is_note_to_self = True
envelope_data = {**envelope_data, "dataMessage": sent_msg}
if not is_note_to_self:
return
# Extract sender info
sender = (
@@ -371,8 +392,8 @@ class SignalAdapter(BasePlatformAdapter):
logger.debug("Signal: ignoring envelope with no sender")
return
# Self-message filtering — prevent reply loops
if self._account_normalized and sender == self._account_normalized:
# Self-message filtering — prevent reply loops (but allow Note to Self)
if self._account_normalized and sender == self._account_normalized and not is_note_to_self:
return
# Filter stories
@@ -577,9 +598,18 @@ class SignalAdapter(BasePlatformAdapter):
result = await self._rpc("send", params)
if result is not None:
self._track_sent_timestamp(result)
return SendResult(success=True)
return SendResult(success=False, error="RPC send failed")
def _track_sent_timestamp(self, rpc_result) -> None:
"""Record outbound message timestamp for echo-back filtering."""
ts = rpc_result.get("timestamp") if isinstance(rpc_result, dict) else None
if ts:
self._recent_sent_timestamps.add(ts)
if len(self._recent_sent_timestamps) > self._max_recent_timestamps:
self._recent_sent_timestamps.pop()
async def send_typing(self, chat_id: str, metadata=None) -> None:
"""Send a typing indicator."""
params: Dict[str, Any] = {
@@ -635,6 +665,7 @@ class SignalAdapter(BasePlatformAdapter):
result = await self._rpc("send", params)
if result is not None:
self._track_sent_timestamp(result)
return SendResult(success=True)
return SendResult(success=False, error="RPC send with attachment failed")
@@ -665,6 +696,7 @@ class SignalAdapter(BasePlatformAdapter):
result = await self._rpc("send", params)
if result is not None:
self._track_sent_timestamp(result)
return SendResult(success=True)
return SendResult(success=False, error="RPC send document failed")

View File

@@ -177,6 +177,19 @@ All phone numbers are automatically redacted in logs:
- `+15551234567``+155****4567`
- This applies to both Hermes gateway logs and the global redaction system
### Note to Self (Single-Number Setup)
If you run signal-cli as a **linked secondary device** on your own phone number (rather than a separate bot number), you can interact with Hermes through Signal's "Note to Self" feature.
Just send a message to yourself from your phone — signal-cli picks it up and Hermes responds in the same conversation.
**How it works:**
- "Note to Self" messages arrive as `syncMessage.sentMessage` envelopes
- The adapter detects when these are addressed to the bot's own account and processes them as regular inbound messages
- Echo-back protection (sent-timestamp tracking) prevents infinite loops — the bot's own replies are filtered out automatically
**No extra configuration needed.** This works automatically as long as `SIGNAL_ACCOUNT` matches your phone number.
### Health Monitoring
The adapter monitors the SSE connection and automatically reconnects if: