Files
Hermes-Skills/okx-auto-position/references/v4.5.5-signal-timestamp-via-gateway-hook.md

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_messages doesn't preserve message.date into event.message.date for the forwarder's user-side Update object in a queryable way; the bot receives it as a MessageService (forwarded notice), not the original Message.
  • The hook receives the message at hook execution time, which is when the agent loop's _handle_message_with_agent runs — 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 — added get_now_str(), append_timestamp(), called in enqueue_if_signal()
  • No gateway changes (hook already calls signal_inbox.enqueue_if_signal)
  • No process_signal.py changes (staleness check unchanged; v4.4.0 behavior preserved when forwarder does append)