# 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`) ```python 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 `⏱信号时间: ` 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)