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:
@@ -179,6 +179,11 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
# Normalize account for self-message filtering
|
# Normalize account for self-message filtering
|
||||||
self._account_normalized = self.account.strip()
|
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",
|
logger.info("Signal adapter initialized: url=%s account=%s groups=%s",
|
||||||
self.http_url, _redact_phone(self.account),
|
self.http_url, _redact_phone(self.account),
|
||||||
"enabled" if self.group_allow_from else "disabled")
|
"enabled" if self.group_allow_from else "disabled")
|
||||||
@@ -353,10 +358,26 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
# Unwrap nested envelope if present
|
# Unwrap nested envelope if present
|
||||||
envelope_data = envelope.get("envelope", envelope)
|
envelope_data = envelope.get("envelope", envelope)
|
||||||
|
|
||||||
# Filter syncMessage envelopes (sent transcripts, read receipts, etc.)
|
# Handle syncMessage: extract "Note to Self" messages (sent to own account)
|
||||||
# signal-cli may set syncMessage to null vs omitting it, so check key existence
|
# while still filtering other sync events (read receipts, typing, etc.)
|
||||||
|
is_note_to_self = False
|
||||||
if "syncMessage" in envelope_data:
|
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
|
# Extract sender info
|
||||||
sender = (
|
sender = (
|
||||||
@@ -371,8 +392,8 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
logger.debug("Signal: ignoring envelope with no sender")
|
logger.debug("Signal: ignoring envelope with no sender")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Self-message filtering — prevent reply loops
|
# Self-message filtering — prevent reply loops (but allow Note to Self)
|
||||||
if self._account_normalized and sender == self._account_normalized:
|
if self._account_normalized and sender == self._account_normalized and not is_note_to_self:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Filter stories
|
# Filter stories
|
||||||
@@ -577,9 +598,18 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
result = await self._rpc("send", params)
|
result = await self._rpc("send", params)
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
self._track_sent_timestamp(result)
|
||||||
return SendResult(success=True)
|
return SendResult(success=True)
|
||||||
return SendResult(success=False, error="RPC send failed")
|
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:
|
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
||||||
"""Send a typing indicator."""
|
"""Send a typing indicator."""
|
||||||
params: Dict[str, Any] = {
|
params: Dict[str, Any] = {
|
||||||
@@ -635,6 +665,7 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
result = await self._rpc("send", params)
|
result = await self._rpc("send", params)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
self._track_sent_timestamp(result)
|
||||||
return SendResult(success=True)
|
return SendResult(success=True)
|
||||||
return SendResult(success=False, error="RPC send with attachment failed")
|
return SendResult(success=False, error="RPC send with attachment failed")
|
||||||
|
|
||||||
@@ -665,6 +696,7 @@ class SignalAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
result = await self._rpc("send", params)
|
result = await self._rpc("send", params)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
self._track_sent_timestamp(result)
|
||||||
return SendResult(success=True)
|
return SendResult(success=True)
|
||||||
return SendResult(success=False, error="RPC send document failed")
|
return SendResult(success=False, error="RPC send document failed")
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,19 @@ All phone numbers are automatically redacted in logs:
|
|||||||
- `+15551234567` → `+155****4567`
|
- `+15551234567` → `+155****4567`
|
||||||
- This applies to both Hermes gateway logs and the global redaction system
|
- 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
|
### Health Monitoring
|
||||||
|
|
||||||
The adapter monitors the SSE connection and automatically reconnects if:
|
The adapter monitors the SSE connection and automatically reconnects if:
|
||||||
|
|||||||
Reference in New Issue
Block a user