4.2 KiB
v4.5.5 Signal Timestamp via Gateway Hook (2026-08-10)
Why this version exists
The v4.4.0 staleness pipeline (references/signal-staleness-pipeline.md) relied on
the forwarder's info_filter.py to append ⏱信号时间: from event.message.date.
Discovered dead code (2026-09-21): the forwarder's user_handler.py uses
Telethon's native client.forward_messages() API — never invokes the FilterChain
(filter_chain logs 共 0 个过滤器). So is_original_time=1 in DB does literally
nothing. Time-stamped text never reaches the gateway.
New approach: append timestamp at the gateway hook
Since the forwarder can't modify text, and switching to send_message would lose
the "Forwarded from X" indicator users rely on, the cleanest fix is to append
the timestamp inside signal_inbox.enqueue_if_signal() — the value stored in
signal_queue.db.raw_text and passed to process_signal.py. The text the user
sees in QQ will have the timestamp even if the TG forwarded message doesn't.
Code (in signal_inbox.py)
from zoneinfo import ZoneInfo
from datetime import datetime
_TZ_SH = ZoneInfo("Asia/Shanghai")
def get_now_str() -> str:
return datetime.now(_TZ_SH).strftime("%Y-%m-%d %H:%M:%S")
def append_timestamp(text: str) -> str:
"""Append ⏱信号时间: {...} at end of text. Idempotent (skips if already present)."""
if not text:
text = ""
stamp = f"\n\n⏱信号时间: {get_now_str()}"
if "⏱信号时间:" in text:
return text # already stamped
return text + stamp
async def enqueue_if_signal(source, text):
# ... chat_id check ...
text_with_ts = append_timestamp(text) # ← injects hook-receive timestamp
# ... enqueue + process_signal call using text_with_ts ...
Why hook-receive time, not signal-issue time
- We have no way to know when the original TG signal was posted by the trader.
forward_messagesdoesn't preservemessage.dateintoevent.message.datefor the forwarder's user-side Update object in a queryable way; the bot receives it as aMessageService(forwarded notice), not the originalMessage. - The hook receives the message at hook execution time, which is when the agent
loop's
_handle_message_with_agentruns — at most a few hundred ms after the TG polling update. - For staleness purposes (30 min window), this is stricter than the original timestamp — so the v4.4.0 staleness check (signal_stale if age ≥ 30 min) still works correctly. If the hook receives a 31-minute-old signal, it's stale.
Idempotency guard
If the message already contains ⏱信号时间: (forwarder appended it correctly in
the rare path where filter chain did run), append_timestamp skips. Prevents
double-stamping.
Edge case: forwarder-with-info_filter somehow active
If the forwarder's info_filter.py ever does run (e.g. on a different deployment),
the TG forwarded message would have a ⏱信号时间: <forward_message_date> appended.
Gateway hook sees that, skips its own stamp (idempotent), and uses the forwarder's
timestamp for staleness check — which is actually better (closer to true signal
age). This dual-path robustness is why the idempotency guard matters.
Diff vs v4.4.0
| Aspect | v4.4.0 (forwarder-time) | v4.5.5 (gateway-time) |
|---|---|---|
| Where timestamp comes from | event.message.date (TG original) |
datetime.now() at hook run |
| How it's appended | forwarder info_filter.py |
signal_inbox.append_timestamp() |
| Survives forwarder restart | ❌ (filter chain dead code) | ✅ (hook always runs) |
| Staleness check (≥30 min) | Works when timestamp present | Always works (timestamp always present) |
Migration
No action needed — signal_inbox.py updated to v4.5.5 in this commit. Forwarder
is_original_time config is now informational only.
Files touched
~/.hermes/skills/trading/okx-auto-position/scripts/signal_inbox.py— addedget_now_str(),append_timestamp(), called inenqueue_if_signal()- No gateway changes (hook already calls
signal_inbox.enqueue_if_signal) - No
process_signal.pychanges (staleness check unchanged; v4.4.0 behavior preserved when forwarder does append)