diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 539ad8ba5..ad6b8cff2 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -12557,6 +12557,36 @@
"queueDetail": "No open gap",
"completion": "Audit Complete",
"completionDetail": "Telegram monitoring audit"
+ },
+ "learningProof": {
+ "title": "Learning closure evidence",
+ "subtitle": "KM / RAG / MCP / PlayBook / Verifier / AI Agent closes only after the Independent Verifier confirms exactly six durable target writes.",
+ "source": "Evidence source",
+ "blocker": "Current blocker",
+ "states": {
+ "verified": "6/6 durable verified",
+ "candidate": "Candidate present, not verified",
+ "blocked": "No verifiable receipt"
+ },
+ "metrics": {
+ "durableWrites": "Durable writes",
+ "verifiedTargets": "Verified targets",
+ "aiAgentReceipts": "AI Agent receipts"
+ },
+ "sources": {
+ "verified": "Independent Verifier",
+ "metadataOnly": "Verifier has metadata only; durable writes are missing",
+ "consumerCandidate": "Consumer receipt candidate, not independently verified",
+ "alertCardCandidate": "Alert-card learning refs candidate, not independently verified",
+ "unavailable": "No evidence source available"
+ },
+ "blockers": {
+ "loading": "Reading Independent Verifier and durable write receipts.",
+ "unavailable": "Coverage readback is unavailable; this must not be treated as no blocker.",
+ "verifier": "Independent Verifier has not passed; candidate receipts cannot replace verification.",
+ "durableWrites": "Exactly 6/6 durable target-write receipts have not been verified.",
+ "none": "No learning-closure blocker."
+ }
}
}
},
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 60818f432..12efd4ec4 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -12557,6 +12557,36 @@
"queueDetail": "沒有待補 gap",
"completion": "盤點完成",
"completionDetail": "Telegram monitoring audit"
+ },
+ "learningProof": {
+ "title": "Learning closure 證據",
+ "subtitle": "只有 Independent Verifier 精確確認六個 durable target writes,才算 KM / RAG / MCP / PlayBook / Verifier / AI Agent 閉環。",
+ "source": "證據來源",
+ "blocker": "目前卡點",
+ "states": {
+ "verified": "6/6 durable verified",
+ "candidate": "候選存在,尚未驗證",
+ "blocked": "尚無可驗證 receipt"
+ },
+ "metrics": {
+ "durableWrites": "Durable writes",
+ "verifiedTargets": "已驗證目標",
+ "aiAgentReceipts": "AI Agent receipts"
+ },
+ "sources": {
+ "verified": "Independent Verifier",
+ "metadataOnly": "Verifier 僅有 metadata,尚無 durable writes",
+ "consumerCandidate": "Consumer receipt 候選,尚未經獨立驗證",
+ "alertCardCandidate": "Alert-card learning refs 候選,尚未經獨立驗證",
+ "unavailable": "尚無可用證據來源"
+ },
+ "blockers": {
+ "loading": "正在讀取 Independent Verifier 與 durable write receipts。",
+ "unavailable": "Coverage readback 無法取得;不得視為沒有卡點。",
+ "verifier": "Independent Verifier 尚未通過;候選 receipt 不可替代驗證。",
+ "durableWrites": "尚未取得精確 6/6 durable target-write receipts。",
+ "none": "沒有 learning closure 卡點。"
+ }
}
}
},
diff --git a/apps/web/src/app/[locale]/awooop/alerts/page.tsx b/apps/web/src/app/[locale]/awooop/alerts/page.tsx
index 77e066371..f2c97b892 100644
--- a/apps/web/src/app/[locale]/awooop/alerts/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/alerts/page.tsx
@@ -26,10 +26,13 @@ type TelegramMonitoringCoveragePayload = {
active_blockers?: string[] | null;
operator_answer?: {
all_telegram_monitoring_alerts_fully_audited?: boolean | null;
- all_telegram_monitoring_alert_surfaces_have_db_or_log_receipt?: boolean | null;
+ all_telegram_monitoring_alert_surfaces_have_db_or_log_receipt?:
+ | boolean
+ | null;
all_telegram_monitoring_alerts_ai_agent_automation_ready?: boolean | null;
all_known_telegram_egress_routes_controlled?: boolean | null;
km_rag_mcp_playbook_ai_agent_context_ready?: boolean | null;
+ durable_learning_writeback_verified?: boolean | null;
manual_default_terminal_state_allowed?: boolean | null;
critical_break_glass_exception_only?: boolean | null;
} | null;
@@ -43,6 +46,12 @@ type TelegramMonitoringCoveragePayload = {
runtime_km_converted_event_count_7d?: number | null;
effective_ai_alert_context_receipt_total?: number | null;
effective_ai_alert_context_ready_total?: number | null;
+ verified_target_count?: number | null;
+ verified_target_context_write_count?: number | null;
+ verified_ai_agent_context_receipt_count?: number | null;
+ ai_loop_context_receipt_count?: number | null;
+ ai_loop_context_source?: string | null;
+ durable_learning_writeback_ready?: boolean | null;
telegram_monitoring_audit_completion_percent?: number | null;
ai_controlled_gap_queue_count?: number | null;
full_coverage_ready?: boolean | null;
@@ -50,6 +59,7 @@ type TelegramMonitoringCoveragePayload = {
};
const API_BASE = getRuntimeApiBaseUrl();
+const LEARNING_TARGET_COUNT = 6;
const NAV_ITEMS = [
{
@@ -98,7 +108,9 @@ function StatCard({
-
{label}
+
+ {label}
+
{value}
@@ -108,20 +120,24 @@ function StatCard({
"flex h-8 w-8 shrink-0 items-center justify-center border",
tone === "ok" && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
tone === "warn" && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
- tone === "neutral" && "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
+ tone === "neutral" &&
+ "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]",
)}
>
-
{detail}
+
+ {detail}
+
);
}
export default function AwoooPAlertsPage() {
const t = useTranslations("awooop.alerts.aiLoop");
- const [coverage, setCoverage] = useState
(null);
+ const [coverage, setCoverage] =
+ useState(null);
const [coverageLoading, setCoverageLoading] = useState(true);
const refreshCoverage = useCallback(async () => {
@@ -137,7 +153,9 @@ export default function AwoooPAlertsPage() {
},
);
if (response.ok) {
- setCoverage((await response.json()) as TelegramMonitoringCoveragePayload);
+ setCoverage(
+ (await response.json()) as TelegramMonitoringCoveragePayload,
+ );
}
} catch {
setCoverage(null);
@@ -157,8 +175,37 @@ export default function AwoooPAlertsPage() {
const answer = coverage?.operator_answer ?? {};
const blockers = coverage?.active_blockers ?? [];
const ready =
- summary.full_coverage_ready === true
- || coverage?.status === "telegram_alert_monitoring_coverage_ready";
+ summary.full_coverage_ready === true ||
+ coverage?.status === "telegram_alert_monitoring_coverage_ready";
+ const durableLearningReady =
+ summary.durable_learning_writeback_ready === true &&
+ answer.durable_learning_writeback_verified === true;
+ const aiLoopSource = summary.ai_loop_context_source ?? "unavailable";
+ const aiLoopSourceLabel =
+ aiLoopSource === "post_apply_verifier"
+ ? t("learningProof.sources.verified")
+ : aiLoopSource === "post_apply_verifier_without_durable_target_writes"
+ ? t("learningProof.sources.metadataOnly")
+ : aiLoopSource === "unverified_log_controlled_writeback_consumer"
+ ? t("learningProof.sources.consumerCandidate")
+ : aiLoopSource === "unverified_ai_alert_card_learning_writeback_refs"
+ ? t("learningProof.sources.alertCardCandidate")
+ : t("learningProof.sources.unavailable");
+ const learningBlocker =
+ blockers.find((blocker) => blocker.startsWith("telegram_alert_ai_loop_")) ??
+ blockers[0] ??
+ "";
+ const learningBlockerLabel = coverageLoading
+ ? t("learningProof.blockers.loading")
+ : coverage == null
+ ? t("learningProof.blockers.unavailable")
+ : learningBlocker ===
+ "telegram_alert_ai_loop_post_apply_verifier_not_ready"
+ ? t("learningProof.blockers.verifier")
+ : learningBlocker ===
+ "telegram_alert_ai_loop_durable_learning_writeback_not_verified"
+ ? t("learningProof.blockers.durableWrites")
+ : learningBlocker || t("learningProof.blockers.none");
const surfaceValue = coverageLoading
? "-- / --"
: `${formatNumber(summary.monitoring_live_evidence_received_count)} / ${formatNumber(summary.monitoring_surface_count)}`;
@@ -174,7 +221,7 @@ export default function AwoooPAlertsPage() {
detail: t("summary.coverageDetail", {
gaps: formatNumber(summary.monitoring_live_receipt_gap_count),
}),
- tone: ready ? "ok" as const : "warn" as const,
+ tone: ready ? ("ok" as const) : ("warn" as const),
icon: ShieldCheck,
},
{
@@ -187,7 +234,7 @@ export default function AwoooPAlertsPage() {
telegram: formatNumber(summary.runtime_telegram_sent_event_count_7d),
km: formatNumber(summary.runtime_km_converted_event_count_7d),
}),
- tone: ready ? "ok" as const : "neutral" as const,
+ tone: ready ? ("ok" as const) : ("neutral" as const),
icon: Database,
},
{
@@ -195,23 +242,33 @@ export default function AwoooPAlertsPage() {
label: t("summary.aiContext"),
value: aiContextValue,
detail: t("summary.aiContextDetail"),
- tone: answer.km_rag_mcp_playbook_ai_agent_context_ready ? "ok" as const : "warn" as const,
+ tone: answer.km_rag_mcp_playbook_ai_agent_context_ready
+ ? ("ok" as const)
+ : ("warn" as const),
icon: Bot,
},
{
key: "direct",
label: t("summary.direct"),
- value: coverageLoading ? "--" : formatNumber(summary.known_direct_send_gap_count),
+ value: coverageLoading
+ ? "--"
+ : formatNumber(summary.known_direct_send_gap_count),
detail: t("summary.directDetail"),
- tone: answer.all_known_telegram_egress_routes_controlled ? "ok" as const : "warn" as const,
+ tone: answer.all_known_telegram_egress_routes_controlled
+ ? ("ok" as const)
+ : ("warn" as const),
icon: Send,
},
{
key: "queue",
label: t("summary.queue"),
- value: coverageLoading ? "--" : formatNumber(summary.ai_controlled_gap_queue_count),
- detail: blockers[0] ?? t("summary.queueDetail"),
- tone: blockers.length === 0 ? "ok" as const : "warn" as const,
+ value: coverageLoading
+ ? "--"
+ : formatNumber(summary.ai_controlled_gap_queue_count),
+ detail: blockers[0]?.startsWith("telegram_alert_ai_loop_")
+ ? learningBlockerLabel
+ : (blockers[0] ?? t("summary.queueDetail")),
+ tone: blockers.length === 0 ? ("ok" as const) : ("warn" as const),
icon: TriangleAlert,
},
{
@@ -221,11 +278,21 @@ export default function AwoooPAlertsPage() {
? "--"
: formatPercent(summary.telegram_monitoring_audit_completion_percent),
detail: t("summary.completionDetail"),
- tone: ready ? "ok" as const : "neutral" as const,
+ tone: ready ? ("ok" as const) : ("neutral" as const),
icon: Gauge,
},
],
- [answer, blockers, coverageLoading, ready, summary, surfaceValue, aiContextValue, t],
+ [
+ answer,
+ blockers,
+ coverageLoading,
+ ready,
+ summary,
+ surfaceValue,
+ aiContextValue,
+ learningBlockerLabel,
+ t,
+ ],
);
return (
@@ -255,7 +322,10 @@ export default function AwoooPAlertsPage() {
-