diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index d82ffbec..286300c8 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -433,6 +433,114 @@ def _format_awooop_status_chain_lines(
return lines
+def _format_km_stale_completion_lines(summary: dict[str, object] | None) -> list[str]:
+ """Render KM owner-review completion state for Telegram detail/history replies."""
+ if not summary:
+ return []
+ status = str(summary.get("status") or "ok")
+ if status == "fetch_failed":
+ return [
+ "",
+ "📚 KM Owner Review",
+ "狀態: fetch_failed",
+ "下一步: open_awooop_work_items",
+ ]
+
+ items = summary.get("items") if isinstance(summary.get("items"), list) else []
+ first_item = items[0] if items and isinstance(items[0], dict) else None
+ lines = [
+ "",
+ "📚 KM Owner Review",
+ (
+ "Completion queue: "
+ f"ready {_safe_int(summary.get('ready_count'))} / "
+ f"blocked {_safe_int(summary.get('blocked_count'))} / "
+ f"completed {_safe_int(summary.get('completed_count'))} / "
+ f"failed {_safe_int(summary.get('failed_count'))}"
+ ),
+ (
+ "Guardrail: "
+ f"read-only {html.escape(_bool_code(not bool(summary.get('writes_on_read'))))} / "
+ f"batch-write {html.escape(_bool_code(summary.get('batch_writes_allowed')))}"
+ ),
+ ]
+ if first_item:
+ lines += [
+ (
+ "此事件: "
+ f"{html.escape(str(first_item.get('entry_id') or '--'))} / "
+ f"{html.escape(str(first_item.get('readiness') or '--'))}"
+ ),
+ (
+ "下一步: "
+ f"{html.escape(str(first_item.get('next_action') or '--'))}"
+ ),
+ ]
+ else:
+ lines.append("此事件: no_related_owner_review")
+ lines.append("下一步: review_awooop_completion_queue")
+ return lines
+
+
+async def _fetch_km_stale_completion_summary_for_incident(
+ *,
+ incident_id: str,
+ project_id: str,
+) -> dict[str, object] | None:
+ """Fetch read-only completion queue state related to one incident."""
+ if not incident_id:
+ return None
+ try:
+ from src.services.governance_km_stale_review_service import (
+ query_km_stale_owner_review_completion_queue,
+ )
+
+ queue = await asyncio.wait_for(
+ query_km_stale_owner_review_completion_queue(
+ project_id=project_id or "awoooi",
+ status_bucket="all",
+ limit=100,
+ ),
+ timeout=2.5,
+ )
+ items = [
+ item.model_dump(mode="json")
+ for item in queue.items
+ if item.related_incident_id == incident_id
+ ]
+ return {
+ "schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
+ "status": "ok",
+ "project_id": project_id or "awoooi",
+ "incident_id": incident_id,
+ "total": queue.total,
+ "returned": queue.returned,
+ "pending_count": queue.pending_count,
+ "ready_count": queue.ready_count,
+ "blocked_count": queue.blocked_count,
+ "completed_count": queue.completed_count,
+ "failed_count": queue.failed_count,
+ "writes_on_read": queue.writes_on_read,
+ "batch_writes_allowed": queue.batch_writes_allowed,
+ "manual_review_required": queue.manual_review_required,
+ "items": items[:5],
+ }
+ except Exception as exc:
+ logger.debug(
+ "telegram_km_stale_completion_summary_fetch_failed",
+ incident_id=incident_id,
+ project_id=project_id,
+ error=str(exc),
+ )
+ return {
+ "schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
+ "status": "fetch_failed",
+ "project_id": project_id or "awoooi",
+ "incident_id": incident_id,
+ "items": [],
+ }
+
+
async def _fetch_remediation_summary_for_card(
*,
approval_id: str,
@@ -5855,17 +5963,23 @@ class TelegramGateway:
error=str(remediation_exc),
)
+ project_id = getattr(incident, "project_id", None) or "awoooi"
+ km_completion_summary = await _fetch_km_stale_completion_summary_for_incident(
+ incident_id=incident_id,
+ project_id=project_id,
+ )
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
- project_id=getattr(incident, "project_id", None) or "awoooi",
+ project_id=project_id,
)
lines += _format_awooop_status_chain_lines(
truth_chain=truth_chain,
remediation_history=remediation_history,
)
+ lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
gateway_summary = (
(truth_chain.get("mcp") or {})
@@ -5884,6 +5998,7 @@ class TelegramGateway:
lines += _format_awooop_status_chain_lines(
remediation_history=remediation_history,
)
+ lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
await self._send_html_line_message(
@@ -6005,17 +6120,23 @@ class TelegramGateway:
error=str(remediation_exc),
)
+ project_id = getattr(incident, "project_id", None) or "awoooi"
+ km_completion_summary = await _fetch_km_stale_completion_summary_for_incident(
+ incident_id=incident_id,
+ project_id=project_id,
+ )
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
- project_id=getattr(incident, "project_id", None) or "awoooi",
+ project_id=project_id,
)
lines += _format_awooop_status_chain_lines(
truth_chain=truth_chain,
remediation_history=remediation_history,
)
+ lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
lines += _format_automation_quality_lines(
truth_chain.get("automation_quality")
@@ -6029,6 +6150,7 @@ class TelegramGateway:
lines += _format_awooop_status_chain_lines(
remediation_history=remediation_history,
)
+ lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
await self._send_html_line_message(
diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py
index 63e094d0..53594cb0 100644
--- a/apps/api/tests/test_telegram_message_templates.py
+++ b/apps/api/tests/test_telegram_message_templates.py
@@ -177,6 +177,35 @@ def test_awooop_status_chain_lines_show_read_only_manual_gate() -> None:
assert "pending_human_approval" in joined
+def test_km_stale_completion_lines_show_owner_review_queue_state() -> None:
+ """詳情/歷史要顯示 KM owner-review completion queue 是否卡住或可處理。"""
+ lines = telegram_gateway_module._format_km_stale_completion_lines({
+ "schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
+ "status": "ok",
+ "incident_id": "INC-20260513-205814",
+ "ready_count": 10,
+ "blocked_count": 0,
+ "completed_count": 1,
+ "failed_count": 0,
+ "writes_on_read": False,
+ "batch_writes_allowed": False,
+ "items": [
+ {
+ "entry_id": "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf",
+ "readiness": "ready",
+ "next_action": "preview_stale_km_review_completion",
+ }
+ ],
+ })
+
+ joined = "\n".join(lines)
+ assert "KM Owner Review" in joined
+ assert "ready 10" in joined
+ assert "batch-write no" in joined
+ assert "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf" in joined
+ assert "preview_stale_km_review_completion" in joined
+
+
def test_awooop_runs_url_for_incident_uses_public_incident_filter() -> None:
"""Telegram URL button 必須導到公開 AwoooP Run list,並帶 incident filter。"""
url = telegram_gateway_module._awooop_runs_url_for_incident("INC-20260514-F85F21")
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 7a829fe9..385f307f 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -2022,6 +2022,8 @@
"knowledgeNext": "Next action: {action}",
"knowledgeDrafts": "KM review drafts: {drafts}; duplicate drafts: {duplicates}",
"knowledgeStaleCandidates": "Stale KM priority queue: {total}; top {top} / {tier}",
+ "knowledgeCompletionQueue": "Completion queue: ready {ready}; blocked {blocked}; completed {completed}; failed {failed}",
+ "knowledgeCompletionLatest": "Latest completion: {entry} / {readiness}; next {next}",
"knowledgeEmpty": "No recent knowledge_degradation dispatch trail",
"frontendConsole": "This page now reads production APIs instead of a static list",
"mcpReady": "MCP Gateway gate is not currently a top gap",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 39c50654..24a15356 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -2023,6 +2023,8 @@
"knowledgeNext": "下一步:{action}",
"knowledgeDrafts": "KM 審核草稿:{drafts};重複草稿:{duplicates}",
"knowledgeStaleCandidates": "陳舊 KM 優先清單:{total} 筆;最高 {top} / {tier}",
+ "knowledgeCompletionQueue": "Completion queue:可處理 {ready};卡住 {blocked};完成 {completed};失敗 {failed}",
+ "knowledgeCompletionLatest": "最新 completion:{entry} / {readiness};下一步 {next}",
"knowledgeEmpty": "近期沒有 knowledge_degradation dispatch trail",
"frontendConsole": "本頁已改讀 production API,而非靜態清單",
"mcpReady": "MCP Gateway gate 目前未列為主要缺口",
diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
index 48f4e339..2db266a8 100644
--- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx
@@ -1513,6 +1513,8 @@ function buildWorkItems(
telemetry.knowledgeReviewDedupe?.duplicate_draft_total ?? knowledgeDuplicateDrafts;
const knowledgeStaleTotal = telemetry.knowledgeStaleCandidates?.total_stale ?? 0;
const topKnowledgeStaleCandidate = telemetry.knowledgeStaleCandidates?.items?.[0] ?? null;
+ const knowledgeCompletionQueue = telemetry.knowledgeStaleOwnerReviewCompletionQueue;
+ const latestKnowledgeCompletion = knowledgeCompletionQueue?.items?.[0] ?? null;
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -1766,6 +1768,21 @@ function buildWorkItems(
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
+ t("evidence.knowledgeCompletionQueue", {
+ ready: knowledgeCompletionQueue?.ready_count ?? 0,
+ blocked: knowledgeCompletionQueue?.blocked_count ?? 0,
+ completed: knowledgeCompletionQueue?.completed_count ?? 0,
+ failed: knowledgeCompletionQueue?.failed_count ?? 0,
+ }),
+ t("evidence.knowledgeCompletionLatest", {
+ entry: latestKnowledgeCompletion?.entry_id ?? "--",
+ readiness: latestKnowledgeCompletion
+ ? t(
+ `knowledgeGovernance.staleCandidates.completionQueue.readiness.${kmStaleCompletionReadinessKey(latestKnowledgeCompletion.readiness)}` as never
+ )
+ : "--",
+ next: latestKnowledgeCompletion?.next_action ?? "--",
+ }),
]
: [
t("evidence.knowledgeEmpty"),
@@ -1774,6 +1791,12 @@ function buildWorkItems(
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
+ t("evidence.knowledgeCompletionQueue", {
+ ready: knowledgeCompletionQueue?.ready_count ?? 0,
+ blocked: knowledgeCompletionQueue?.blocked_count ?? 0,
+ completed: knowledgeCompletionQueue?.completed_count ?? 0,
+ failed: knowledgeCompletionQueue?.failed_count ?? 0,
+ }),
],
href: "/awooop/work-items",
},