diff --git a/apps/api/src/services/awooop_truth_chain_service.py b/apps/api/src/services/awooop_truth_chain_service.py
index 413dce68..53cb947d 100644
--- a/apps/api/src/services/awooop_truth_chain_service.py
+++ b/apps/api/src/services/awooop_truth_chain_service.py
@@ -785,6 +785,8 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
AND (
run_id::text = :source_id
OR content_preview ILIKE :needle
+ OR coalesce(source_envelope #> '{source_refs,incident_ids}', '[]'::jsonb) ? :source_id
+ OR coalesce(source_envelope #> '{source_refs,code_refs}', '[]'::jsonb) ? :source_id
)
ORDER BY queued_at DESC
LIMIT :limit
diff --git a/apps/api/src/services/channel_hub.py b/apps/api/src/services/channel_hub.py
index ff60ea5e..4d61830c 100644
--- a/apps/api/src/services/channel_hub.py
+++ b/apps/api/src/services/channel_hub.py
@@ -514,7 +514,7 @@ async def record_outbound_message(
content_hash, content_preview, content_redacted,
redaction_version, source_envelope,
provider_message_id,
- send_status, queued_at,
+ send_status, queued_at, sent_at,
triggered_by_state, waiting_since
) VALUES (
:project_id, :run_id, :conversation_event_id,
@@ -523,6 +523,7 @@ async def record_outbound_message(
:redaction_version, CAST(:source_envelope AS jsonb),
:provider_message_id,
:send_status, NOW(),
+ CASE WHEN :send_status = 'sent' THEN NOW() ELSE NULL END,
:triggered_by_state, :waiting_since
)
RETURNING message_id
diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index 87546067..f00d3ef1 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -69,6 +69,7 @@ POLLING_LEADER_WATCH = 30 # seconds - 非 Leader Pod 每 30s 嘗試接管
logger = structlog.get_logger(__name__)
_TELEGRAM_BOT_URL_RE = re.compile(r"(api\.telegram\.org/bot)[^/\s]+")
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
+_CODE_REF_RE = re.compile(r"([0-9a-f]{7,12})", re.IGNORECASE)
def _top_gateway_bucket(
@@ -213,6 +214,9 @@ def _reply_markup_summary(payload: dict) -> dict[str, object]:
def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
"""Build a redaction-friendly source envelope for Channel Hub replay."""
+ text = str(payload.get("text") or payload.get("caption") or "")
+ incident_ids = sorted(set(_INCIDENT_ID_RE.findall(text)))
+ code_refs = sorted(set(match.group(1) for match in _CODE_REF_RE.finditer(text)))
return {
"adapter": "legacy_telegram_gateway",
"method": method,
@@ -222,6 +226,10 @@ def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
"disable_web_page_preview": payload.get("disable_web_page_preview"),
"has_reply_context": _has_reply_context(payload),
"reply_markup": _reply_markup_summary(payload),
+ "source_refs": {
+ "incident_ids": incident_ids[:20],
+ "code_refs": code_refs[:20],
+ },
}
# 2026-04-27 Claude Sonnet 4.6: B3 — LLM 動態 Telegram 按鈕 Feature Flag
diff --git a/apps/api/tests/test_channel_hub_grouped_alert_events.py b/apps/api/tests/test_channel_hub_grouped_alert_events.py
index eb22386c..230b3e75 100644
--- a/apps/api/tests/test_channel_hub_grouped_alert_events.py
+++ b/apps/api/tests/test_channel_hub_grouped_alert_events.py
@@ -6,6 +6,7 @@ from src.services.channel_hub import (
ensure_completed_shadow_run,
format_grouped_alert_digest_text,
format_grouped_alert_event_content,
+ record_outbound_message,
)
@@ -79,10 +80,14 @@ class _FakeSession:
def __init__(self) -> None:
self.statement = ""
self.params = {}
+ self.statements = []
+ self.param_sets = []
async def execute(self, statement, params): # noqa: ANN001
self.statement = str(statement)
self.params = params
+ self.statements.append(str(statement))
+ self.param_sets.append(params)
return _FakeResult()
@@ -105,3 +110,27 @@ async def test_completed_shadow_run_sets_run_state_not_null_defaults() -> None:
assert "0, 3, 0.0000, 0" in session.statement
assert session.params["project_id"] == "awoooi"
assert session.params["run_id"] == run_id
+
+
+async def test_record_outbound_message_sets_sent_at_for_sent_messages() -> None:
+ session = _FakeSession()
+ run_id = build_grouped_alert_run_id("awoooi", "telegram-message-13152")
+
+ await record_outbound_message(
+ session, # type: ignore[arg-type]
+ project_id="awoooi",
+ run_id=run_id,
+ channel_type="telegram",
+ channel_chat_id="-100123",
+ message_type="approval_request",
+ content="ACTION REQUIRED INC-20260513-9B082D",
+ provider_message_id="13152",
+ send_status="sent",
+ triggered_by_state="legacy_gateway",
+ is_shadow=False,
+ )
+
+ insert_statement = session.statements[-1]
+ assert "sent_at" in insert_statement
+ assert "CASE WHEN :send_status = 'sent' THEN NOW() ELSE NULL END" in insert_statement
+ assert session.param_sets[-1]["send_status"] == "sent"
diff --git a/apps/api/tests/test_telegram_gateway_error_sanitizer.py b/apps/api/tests/test_telegram_gateway_error_sanitizer.py
index 913a0848..5edffa5a 100644
--- a/apps/api/tests/test_telegram_gateway_error_sanitizer.py
+++ b/apps/api/tests/test_telegram_gateway_error_sanitizer.py
@@ -18,7 +18,11 @@ def test_telegram_gateway_sanitizes_bot_token_url() -> None:
def test_outbound_source_envelope_keeps_replay_context_without_raw_payload() -> None:
payload = {
"chat_id": "-100123",
- "text": "ACTION REQUIRED token 1234567890:abcdefghijklmnopqrstuvwxyzABCDEFGH",
+ "text": (
+ "ACTION REQUIRED INC-20260513-9B082D "
+ "7f858956 token "
+ "1234567890:abcdefghijklmnopqrstuvwxyzABCDEFGH"
+ ),
"parse_mode": "HTML",
"reply_markup": {
"inline_keyboard": [
@@ -40,6 +44,8 @@ def test_outbound_source_envelope_keeps_replay_context_without_raw_payload() ->
assert envelope["reply_markup"]["button_count"] == 2
assert envelope["reply_markup"]["buttons"][0]["callback_prefix"] == "approve"
assert envelope["reply_markup"]["buttons"][1]["callback_prefix"] == "details"
+ assert envelope["source_refs"]["incident_ids"] == ["INC-20260513-9B082D"]
+ assert envelope["source_refs"]["code_refs"] == ["7f858956"]
assert "approval-id-secret" not in str(envelope)
assert "1234567890:" not in str(envelope)
assert "ACTION REQUIRED" not in str(envelope)
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 53c6b323..aceee09d 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -7251,3 +7251,57 @@ scope=write
- T11 已部署:Operator Run Detail 與 Telegram detail formatter 都已能呈現 MCP Gateway 摘要。
- 這次沒有發送真實 Telegram 測試訊息,避免洗版;改以 production pod 直接呼叫 detail formatter 驗證。
- 目前整體進度更新:約 68%。
+
+### 2026-05-13 — AwoooP truth-chain T12a:Telegram outbound 可回放關聯強化(local green)
+
+**production live audit 摘要**:
+
+```text
+24h:
+incidents=150
+approval_records=102
+alert_operation_log=682
+timeline_events=154
+incident_evidence=130
+auto_repair_executions=10
+knowledge_entries=42
+legacy_mcp_audit_log=1265
+awooop_mcp_gateway_audit=1365
+awooop_outbound_message=420
+
+outbound_quality:
+total=420 redacted=226 envelope=420 envelope_schema=226 with_run=420 sent_at=0
+
+incident_join_quality:
+total=150 with_aol=100 with_evidence=102 with_legacy_mcp=102 with_timeline=102 with_approval=102 with_auto_repair=10
+
+Sentry / SignOz durable event tables:
+none found by information_schema table_name ILIKE '%sentry%' OR '%signoz%'
+```
+
+**判讀**:
+
+- MCP / SignOz 能力已有實際使用,且 legacy MCP bridge 已寫入 AwoooP Gateway audit。
+- 仍不能宣稱完整 AI 自動修復:24h incident 150 筆中只有 10 筆有 `auto_repair_executions`。
+- Telegram outbound mirror 有資料,但 `sent_at=0` 是真缺口;source envelope 也缺少 structured source refs,truth-chain 只能靠 `content_preview ILIKE` 猜關聯。
+
+**變更**:
+
+- `record_outbound_message()` 在 `send_status='sent'` 時寫入 `sent_at=NOW()`。
+- Telegram outbound `source_envelope` 新增 `source_refs.incident_ids` 與 `source_refs.code_refs`,保留 redaction-friendly 關聯錨點。
+- truth-chain outbound 查詢支援 structured `source_refs`,不只靠 preview 文字搜尋。
+
+**local verification**:
+
+```text
+DATABASE_URL=postgresql+asyncpg://u:p@localhost:5432/db python -m pytest tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_awooop_truth_chain_service.py tests/test_telegram_adr050.py -q
+51 passed
+
+python -m ruff check --select F821 src/services/channel_hub.py src/services/telegram_gateway.py src/services/awooop_truth_chain_service.py tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_telegram_adr050.py
+All checks passed
+
+python -m py_compile src/services/channel_hub.py src/services/telegram_gateway.py src/services/awooop_truth_chain_service.py tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_telegram_adr050.py
+OK
+```
+
+**目前整體進度**:約 69%。