diff --git a/apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py b/apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py index d9358a685..0fbda0f70 100644 --- a/apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py +++ b/apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py @@ -22,11 +22,16 @@ from src.services.ai_agent_log_controlled_writeback_dispatch import ( EXECUTOR_ROUTE, OPERATION_TYPE, ) +from src.services.platform_operator_service import list_ai_alert_card_delivery_readback SCHEMA_VERSION = "ai_agent_log_controlled_writeback_consumer_readback_v1" +TELEGRAM_ALERT_CONTEXT_SCHEMA_VERSION = ( + "telegram_alert_ai_loop_consumer_context_readback_v1" +) DEFAULT_PROJECT_ID = "awoooi" CONSUMER_OPERATION_TYPE = "log_controlled_writeback_consumed" CONSUMER_EXECUTOR_ROUTE = "ai_agent_metadata_writeback_consumer" +TELEGRAM_ALERT_CONTEXT_PER_PAGE = 6 _TARGETS = ("km", "rag", "playbook", "mcp", "verifier", "ai_agent") _CONSUMER_SURFACES = { "km": "knowledge_memory_context", @@ -59,7 +64,14 @@ async def load_latest_ai_agent_log_controlled_writeback_consumer_readback( error_type=type(exc).__name__, ) - return _build_consumer_readback(rows=rows, consumer_rows=consumer_rows) + telegram_alert_learning_context = await _load_telegram_alert_learning_context( + project_id=project_id + ) + return _build_consumer_readback( + rows=rows, + consumer_rows=consumer_rows, + telegram_alert_learning_context=telegram_alert_learning_context, + ) async def _load_consumer_rows_with_retry( @@ -161,6 +173,7 @@ def _build_consumer_readback( *, rows: list[dict[str, Any]], consumer_rows: list[dict[str, Any]], + telegram_alert_learning_context: dict[str, Any], ) -> dict[str, Any]: consumer_receipts = _consumer_receipts_by_dispatch_id(consumer_rows) bindings = [_consumer_binding(row, consumer_receipts) for row in rows] @@ -251,13 +264,37 @@ def _build_consumer_readback( "ai_agent_context_receipt_write_count": _target_write_count( bindings, "ai_agent" ), + "telegram_alert_learning_context_readback_ready": ( + telegram_alert_learning_context.get("status") + == "ai_loop_agent_context_receipt_ready" + ), + "telegram_alert_learning_context_receipt_count": _safe_int( + telegram_alert_learning_context.get("context_receipt_count") + ), + "telegram_alert_learning_ai_agent_context_receipt_count": _safe_int( + telegram_alert_learning_context.get( + "ai_agent_context_receipt_count" + ) + ), + "telegram_alert_learning_ready_target_count": _safe_int( + telegram_alert_learning_context.get("ready_target_count") + ), + "telegram_alert_learning_target_count": _safe_int( + telegram_alert_learning_context.get("target_count") + ), "runtime_target_write_performed": runtime_target_write_performed, }, + "telegram_alert_learning_context": telegram_alert_learning_context, "active_blockers": active_blockers, "operation_boundaries": { "consumer_readback_only": True, "metadata_ledger_read_performed": True, "consumer_apply_receipt_read_performed": True, + "telegram_alert_registry_metadata_read_performed": ( + telegram_alert_learning_context["operation_boundaries"][ + "metadata_read_performed" + ] + ), "runtime_target_write_performed": runtime_target_write_performed, "km_write_performed": False, "rag_index_write_performed": False, @@ -335,8 +372,17 @@ def _fallback_consumer_readback( "mcp_context_receipt_write_count": 0, "verifier_context_receipt_write_count": 0, "ai_agent_context_receipt_write_count": 0, + "telegram_alert_learning_context_readback_ready": False, + "telegram_alert_learning_context_receipt_count": 0, + "telegram_alert_learning_ai_agent_context_receipt_count": 0, + "telegram_alert_learning_ready_target_count": 0, + "telegram_alert_learning_target_count": 0, "runtime_target_write_performed": False, }, + "telegram_alert_learning_context": _telegram_alert_learning_context_unavailable( + project_id=project_id, + error_type=error_type, + ), "active_blockers": [ "log_controlled_writeback_consumer_readback_db_unavailable" ], @@ -344,6 +390,7 @@ def _fallback_consumer_readback( "consumer_readback_only": True, "metadata_ledger_read_performed": False, "consumer_apply_receipt_read_performed": False, + "telegram_alert_registry_metadata_read_performed": False, "runtime_target_write_performed": False, "km_write_performed": False, "rag_index_write_performed": False, @@ -553,6 +600,224 @@ def _consumer_receipts_by_dispatch_id( return receipts +async def _load_telegram_alert_learning_context( + *, + project_id: str, +) -> dict[str, Any]: + """Read Telegram alert learning registry as AI Loop consumer context.""" + try: + readback = await list_ai_alert_card_delivery_readback( + project_id=project_id, + page=1, + per_page=TELEGRAM_ALERT_CONTEXT_PER_PAGE, + refresh=True, + ) + except Exception as exc: # pragma: no cover - live DB pool pressure + logger.warning( + "telegram_alert_learning_context_readback_unavailable", + project_id=project_id, + error_type=type(exc).__name__, + ) + return _telegram_alert_learning_context_unavailable( + project_id=project_id, + error_type=type(exc).__name__, + ) + return _telegram_alert_learning_context_from_alert_cards(readback) + + +def _telegram_alert_learning_context_from_alert_cards( + readback: Mapping[str, Any], +) -> dict[str, Any]: + registry = _dict(readback.get("learning_registry")) + summary = _dict(readback.get("summary")) + project_id = str( + registry.get("project_id") + or summary.get("project_id") + or DEFAULT_PROJECT_ID + ) + items = [_dict(item) for item in _list(readback.get("items"))] + context_receipts = _telegram_alert_context_receipts(items) + target_count = _safe_int(registry.get("target_count") or len(_TARGETS)) + ready_target_count = _safe_int(registry.get("ready_target_count")) + ai_agent_context_receipt_count = sum( + 1 for receipt in context_receipts if receipt["target"] == "ai_agent" + ) + ready = ( + registry.get("status") == "learning_registry_ready" + and ready_target_count == target_count + and target_count > 0 + and ai_agent_context_receipt_count > 0 + ) + + return { + "schema_version": TELEGRAM_ALERT_CONTEXT_SCHEMA_VERSION, + "status": ( + "ai_loop_agent_context_receipt_ready" + if ready + else "waiting_telegram_alert_learning_registry" + ), + "priority_work_item_id": "CIR-P0-TG-001", + "project_id": project_id, + "source_endpoint": "/api/v1/platform/runs/ai-alert-cards", + "source_registry_schema_version": registry.get("schema_version"), + "source_registry_status": registry.get("status"), + "consumer_context_route": "ai_loop_agent.telegram_alert_learning_context", + "target_count": target_count, + "ready_target_count": ready_target_count, + "context_receipt_count": len(context_receipts), + "ai_agent_context_receipt_count": ai_agent_context_receipt_count, + "next_action": ( + "verify_telegram_alert_learning_context_receipts_with_ai_loop_post_apply_verifier" + if ready + else "wait_for_telegram_alert_learning_registry_before_context_receipt" + ), + "context_receipts": context_receipts, + "active_blockers": ( + [] + if ready + else ["telegram_alert_learning_registry_not_ready_for_ai_loop_context"] + ), + "operation_boundaries": { + "metadata_read_performed": True, + "db_or_log_receipt_read_performed": bool(items), + "runtime_target_write_performed": False, + "km_write_performed": False, + "rag_index_write_performed": False, + "playbook_trust_write_performed": False, + "mcp_tool_call_performed": False, + "agent_runtime_action_performed": False, + "telegram_send_performed": False, + "raw_payload_included": False, + "secret_value_collection_allowed": False, + "github_api_used": False, + }, + } + + +def _telegram_alert_context_receipts( + items: list[dict[str, Any]], +) -> list[dict[str, Any]]: + receipts: list[dict[str, Any]] = [] + for item in items: + learning_refs = _dict(item.get("learning_writeback_refs")) + message_id = str(item.get("message_id") or learning_refs.get("receipt_id") or "") + run_id = str(item.get("run_id") or learning_refs.get("run_id") or "") + for binding_value in _list(item.get("learning_registry_bindings")): + binding = _dict(binding_value) + target = str(binding.get("target") or "") + if target not in _TARGETS: + continue + ref = str(binding.get("ref") or "") + status = str(binding.get("status") or "") + ready = status == "ready_for_consumer_context" and bool(ref) + receipts.append({ + "receipt_id": ( + "telegram_alert_learning_context::" + f"{message_id or 'unknown-message'}::{target}" + ), + "source_receipt_id": str(learning_refs.get("receipt_id") or message_id), + "source_run_id": run_id, + "source_message_id": message_id, + "work_item_id": str( + binding.get("work_item_id") + or learning_refs.get("work_item_id") + or "CIR-P0-TG-001" + ), + "target": target, + "consumer_surface": str(binding.get("consumer_surface") or ""), + "source_ref": ref, + "ai_agent_context_ref": str( + learning_refs.get("ai_agent_context_ref") or "" + ), + "event_type": str(item.get("event_type") or ""), + "lane": str(item.get("lane") or ""), + "status": ( + "ready_for_ai_loop_context" if ready else "missing_source_ref" + ), + "metadata_only": True, + "target_write_performed": False, + "raw_payload_included": False, + "target_selector": { + "project_id": str(item.get("project_id") or DEFAULT_PROJECT_ID), + "run_id": run_id, + "message_id": message_id, + "target": target, + "source_ref": ref, + }, + "source_of_truth_diff": { + "current_state": "telegram_alert_learning_registry_readable", + "desired_state": "ai_loop_agent_context_receipt_available", + "delta_kind": f"telegram_alert_{target}_context_binding", + "raw_payload_included": False, + }, + "check_mode": { + "enabled": True, + "checks": [ + "ai_alert_card_delivery_receipt_present", + "learning_registry_binding_ready", + "metadata_only_raw_payload_absent", + "ai_agent_context_ref_present", + ], + }, + "rollback": { + "required": True, + "rollback_ref": ( + "rollback://telegram-alert-learning-context/" + f"{message_id or 'unknown-message'}/{target}" + ), + "strategy": "ignore_context_receipt_and_wait_for_next_delivery_readback", + }, + "post_apply_verifier": { + "required": True, + "verifier_refs": _unique([ + str(learning_refs.get("post_verifier_ref") or ""), + str(binding.get("ref") or ""), + ]), + }, + }) + return receipts + + +def _telegram_alert_learning_context_unavailable( + *, + project_id: str, + error_type: str, +) -> dict[str, Any]: + return { + "schema_version": TELEGRAM_ALERT_CONTEXT_SCHEMA_VERSION, + "status": "blocked_telegram_alert_learning_registry_readback_unavailable", + "priority_work_item_id": "CIR-P0-TG-001", + "project_id": project_id, + "source_endpoint": "/api/v1/platform/runs/ai-alert-cards", + "source_registry_schema_version": None, + "source_registry_status": "unavailable", + "consumer_context_route": "ai_loop_agent.telegram_alert_learning_context", + "target_count": len(_TARGETS), + "ready_target_count": 0, + "context_receipt_count": 0, + "ai_agent_context_receipt_count": 0, + "next_action": "retry_telegram_alert_learning_registry_readback", + "context_receipts": [], + "active_blockers": [ + f"telegram_alert_learning_registry_readback_unavailable:{error_type}" + ], + "operation_boundaries": { + "metadata_read_performed": False, + "db_or_log_receipt_read_performed": False, + "runtime_target_write_performed": False, + "km_write_performed": False, + "rag_index_write_performed": False, + "playbook_trust_write_performed": False, + "mcp_tool_call_performed": False, + "agent_runtime_action_performed": False, + "telegram_send_performed": False, + "raw_payload_included": False, + "secret_value_collection_allowed": False, + "github_api_used": False, + }, + } + + def _result_rows(result: Any) -> list[dict[str, Any]]: mappings = getattr(result, "mappings", None) if callable(mappings): @@ -574,16 +839,39 @@ def _row_mapping(row: Any) -> dict[str, Any]: def _strings(value: Any) -> list[str]: if isinstance(value, list): - return [str(item) for item in value] + return [str(item) for item in value if str(item)] if isinstance(value, tuple): - return [str(item) for item in value] + return [str(item) for item in value if str(item)] return [] +def _dict(value: Any) -> dict[str, Any]: + if isinstance(value, Mapping): + return dict(value) + return {} + + +def _list(value: Any) -> list[Any]: + if isinstance(value, list): + return value + if isinstance(value, tuple): + return list(value) + return [] + + +def _safe_int(value: Any) -> int: + try: + return int(value) + except (TypeError, ValueError): + return 0 + + def _unique(values: list[str]) -> list[str]: seen = set() result = [] for value in values: + if not value: + continue if value in seen: continue seen.add(value) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 7eae0c282..6537d67da 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -340,10 +340,11 @@ _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ "已改為 AWOOI API / TelegramGateway receipt path;目前明確讀回 workflow direct send 0、" "ops script direct gap 0、API direct gap 0;/runs/ai-alert-cards 已補 KM / PlayBook / RAG / MCP " "learning_writeback_refs read model,並投影 KM / PlayBook / RAG / MCP / verifier / AI Agent " - "learning_registry consumer bindings。" + "learning_registry consumer bindings;agent-log-controlled-writeback-consumer-readback 已把同一批 " + "Telegram alert registry 轉成 AI Loop Agent consumer context receipt。" ), - "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gap、learning refs 與 registry readback。", - "next_action": "把 Telegram alert learning registry 接到 AI Loop Agent consumer context receipt,讓後續判斷可直接重用這些 receipt。", + "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap、direct send gap、learning refs、registry readback 與 AI Loop consumer context receipt。", + "next_action": "把 Telegram alert AI Loop context receipts 接 post-apply verifier,讓後續判斷可直接重用這些 receipt。", "mapped_workplan_id": "P0-006", }, { @@ -3809,6 +3810,7 @@ def apply_ai_automation_live_closure_readbacks( consumer = _dict(consumer_readback) consumer_rollups = _dict(consumer.get("rollups")) consumer_control = _dict(consumer.get("controlled_consume")) + telegram_context = _dict(consumer.get("telegram_alert_learning_context")) consumer_active_blockers = _strings(consumer.get("active_blockers")) if consumer: context_receipt_counts = { @@ -3851,6 +3853,27 @@ def apply_ai_automation_live_closure_readbacks( summary["ai_automation_consumer_apply_route"] = str( consumer_control.get("consumer_apply_route") or "" ) + summary["ai_automation_telegram_alert_context_status"] = str( + telegram_context.get("status") or "" + ) + summary["ai_automation_telegram_alert_context_readback_ready"] = bool( + consumer_rollups.get("telegram_alert_learning_context_readback_ready") + is True + ) + summary["ai_automation_telegram_alert_context_receipt_count"] = _int( + consumer_rollups.get("telegram_alert_learning_context_receipt_count") + ) + summary["ai_automation_telegram_alert_ai_agent_context_receipt_count"] = _int( + consumer_rollups.get( + "telegram_alert_learning_ai_agent_context_receipt_count" + ) + ) + summary["ai_automation_telegram_alert_context_ready_target_count"] = _int( + consumer_rollups.get("telegram_alert_learning_ready_target_count") + ) + summary["ai_automation_telegram_alert_context_target_count"] = _int( + consumer_rollups.get("telegram_alert_learning_target_count") + ) summary["ai_automation_consumer_active_blockers"] = consumer_active_blockers rollups["ai_automation_consumer_active_blocker_count"] = len( consumer_active_blockers @@ -3861,6 +3884,12 @@ def apply_ai_automation_live_closure_readbacks( rollups["ai_automation_consumer_target_context_receipt_write_count"] = ( summary["ai_automation_consumer_target_context_receipt_write_count"] ) + rollups["ai_automation_telegram_alert_context_receipt_count"] = summary[ + "ai_automation_telegram_alert_context_receipt_count" + ] + rollups["ai_automation_telegram_alert_ai_agent_context_receipt_count"] = ( + summary["ai_automation_telegram_alert_ai_agent_context_receipt_count"] + ) state["ai_automation_consumer_readback_ready"] = summary[ "ai_automation_consumer_readback_ready" ] @@ -3939,6 +3968,15 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: consumer_context_counts = _dict( summary.get("ai_automation_consumer_context_receipt_counts") ) + telegram_alert_context_ready = bool( + summary.get("ai_automation_telegram_alert_context_readback_ready") is True + ) + telegram_alert_context_receipt_count = _int( + summary.get("ai_automation_telegram_alert_context_receipt_count") + ) + telegram_alert_ai_agent_context_receipt_count = _int( + summary.get("ai_automation_telegram_alert_ai_agent_context_receipt_count") + ) consumer_apply_route = str(summary.get("ai_automation_consumer_apply_route") or "") normalizer_ready = bool( queue_fields @@ -3948,7 +3986,8 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: ) candidate_ready = bool(safe_next_action_id or executor_dispatch_ready) execution_boundary_ready = bool( - consumer_runtime_target_write_performed + telegram_alert_context_ready + or consumer_runtime_target_write_performed or executor_runtime_dispatch_performed or executor_dispatch_ready ) @@ -3958,9 +3997,15 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: or consumer_post_apply_verifier_ref_count > 0 ) learning_writeback_ready = bool( - consumer_readback_ready - and consumer_context_receipt_write_count >= consumer_ready_target_count - and consumer_ready_target_count > 0 + ( + consumer_readback_ready + and consumer_context_receipt_write_count >= consumer_ready_target_count + and consumer_ready_target_count > 0 + ) + or ( + telegram_alert_context_ready + and telegram_alert_ai_agent_context_receipt_count > 0 + ) ) base_refs = [ @@ -4055,10 +4100,15 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: "no_secret_no_runtime_secret", f"consumer_ready_targets:{consumer_ready_target_count}", f"context_receipts:{consumer_context_receipt_write_count}", + f"telegram_alert_context_receipts:{telegram_alert_context_receipt_count}", + ( + "telegram_alert_ai_agent_context_receipts:" + f"{telegram_alert_ai_agent_context_receipt_count}" + ), f"consumer_apply_route:{consumer_apply_route}", ], "verifier_result": "controlled_consumer_context_receipt_readback" - if consumer_runtime_target_write_performed + if consumer_runtime_target_write_performed or telegram_alert_context_ready else ( "controlled_executor_dispatch_ready" if executor_dispatch_ready @@ -4103,6 +4153,7 @@ def _apply_ai_automation_node_receipts(payload: dict[str, Any]) -> None: f"rag_context_receipt:{_int(consumer_context_counts.get('rag'))}", f"playbook_context_receipt:{_int(consumer_context_counts.get('playbook'))}", f"mcp_context_receipt:{_int(consumer_context_counts.get('mcp'))}", + f"telegram_alert_ai_agent_context_receipt:{telegram_alert_ai_agent_context_receipt_count}", ], "verifier_result": "consumer_context_receipt_writeback_ready" if learning_writeback_ready diff --git a/apps/api/src/services/telegram_alert_ai_automation_matrix.py b/apps/api/src/services/telegram_alert_ai_automation_matrix.py index a0ca88639..25d8daaa2 100644 --- a/apps/api/src/services/telegram_alert_ai_automation_matrix.py +++ b/apps/api/src/services/telegram_alert_ai_automation_matrix.py @@ -87,6 +87,9 @@ def load_latest_telegram_alert_ai_automation_matrix( learning_registry_present = bool( source_proof["ai_alert_card_learning_registry_readback_present"] ) + ai_loop_context_present = bool( + source_proof["telegram_alert_ai_loop_consumer_context_readback_present"] + ) direct_gap_count = int(egress_summary["direct_bot_api_call_count"]) direct_gap_file_count = int(egress_summary["direct_bot_api_file_count"]) @@ -273,6 +276,46 @@ def load_latest_telegram_alert_ai_automation_matrix( "/api/v1/platform/runs/ai-alert-cards", ], }, + { + "surface_id": "telegram_ai_loop_agent_consumer_context", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "ai_loop_agent_consumer_context", + "status": ( + "ai_loop_context_readback_present" + if ai_loop_context_present + else "ai_loop_context_readback_missing" + ), + "known_item_count": source_proof[ + "telegram_alert_ai_loop_consumer_context_readback_count" + ], + "db_or_log_receipt": "ready_ai_alert_card_registry_receipts", + "ai_route": ( + "ready_ai_loop_agent_context_reuse" + if ai_loop_context_present + else "missing_ai_loop_agent_context_reuse" + ), + "controlled_queue": ( + "ready_metadata_only_context_receipt" + if ai_loop_context_present + else "missing_context_receipt" + ), + "post_verifier": ( + "ready_ai_loop_context_source_contract_present" + if ai_loop_context_present + else "missing_ai_loop_context_source_contract" + ), + "learning_writeback": ( + "ready_agent_context_receipt_readback" + if ai_loop_context_present + else "missing_agent_context_receipt_readback" + ), + "manual_default_gap_count": 0, + "direct_send_gap_count": 0, + "evidence_refs": [ + "apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py", + "/api/v1/agents/agent-log-controlled-writeback-consumer-readback", + ], + }, ] next_priority_action = _next_priority_action( @@ -281,6 +324,7 @@ def load_latest_telegram_alert_ai_automation_matrix( direct_gap_count=direct_gap_count, learning_writeback_refs_present=learning_writeback_refs_present, learning_registry_present=learning_registry_present, + ai_loop_context_present=ai_loop_context_present, ) next_controlled_actions = _ordered_controlled_actions(next_priority_action) summary = _build_summary( @@ -338,6 +382,12 @@ def load_latest_telegram_alert_ai_automation_matrix( ( ( ( + "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; " + "Telegram alert registry is now consumable by AI Loop Agent context, " + "so the next P0 step is post-apply verifier receipt." + ) + if ai_loop_context_present + else ( "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; " "KM / PlayBook / RAG / MCP / verifier / AI Agent registry readback is present, " "so the next P0 step is AI Loop Agent consumer context." @@ -489,6 +539,10 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: "apps/api/src/services/platform_operator_service.py", "ai_agent_context_ref", ), + "telegram_alert_ai_loop_consumer_context_readback_present": ( + "apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py", + "telegram_alert_ai_loop_consumer_context_readback_v1", + ), } result: dict[str, int | bool] = {} for key, (relative_path, marker) in source_checks.items(): @@ -503,6 +557,10 @@ def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: result["ai_alert_card_learning_registry_readback_count"] = ( 1 if present else 0 ) + if key == "telegram_alert_ai_loop_consumer_context_readback_present": + result["telegram_alert_ai_loop_consumer_context_readback_count"] = ( + 1 if present else 0 + ) required = [key for key, value in result.items() if key.endswith("_present") and value is not True] if required: @@ -529,6 +587,7 @@ def _next_priority_action( direct_gap_count: int, learning_writeback_refs_present: bool, learning_registry_present: bool, + ai_loop_context_present: bool, ) -> dict[str, Any]: if api_direct_gap_count > 0: return { @@ -580,14 +639,24 @@ def _next_priority_action( "requires_runtime_send": False, "post_verifier": "KM / PlayBook / RAG / MCP registry readback includes CIR-P0-TG-001 alert-card receipt refs.", } + if not ai_loop_context_present: + return { + "action_id": "consume_telegram_alert_learning_registry_in_ai_loop_agent_context", + "scope": "KM / PlayBook / RAG / MCP / verifier registry -> AI Loop Agent consumer context", + "priority": "P0", + "why": "Telegram alert learning registry is readable; the next closure is AI Loop Agent consumer context reuse and apply receipt.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "AI Loop Agent context readback references the CIR-P0-TG-001 Telegram alert learning registry.", + } return { - "action_id": "consume_telegram_alert_learning_registry_in_ai_loop_agent_context", - "scope": "KM / PlayBook / RAG / MCP / verifier registry -> AI Loop Agent consumer context", + "action_id": "verify_telegram_alert_learning_context_receipts_with_ai_loop_post_apply_verifier", + "scope": "AI Loop Agent Telegram alert consumer context -> post-apply verifier receipt", "priority": "P0", - "why": "Telegram alert learning registry is readable; the next closure is AI Loop Agent consumer context reuse and apply receipt.", + "why": "Telegram alert learning registry is readable by AI Loop Agent context; the next closure is verifier receipt and controlled apply reuse.", "requires_secret": False, "requires_runtime_send": False, - "post_verifier": "AI Loop Agent context readback references the CIR-P0-TG-001 Telegram alert learning registry.", + "post_verifier": "AI Loop post-apply verifier reads Telegram alert context receipts and reports reusable decision context.", } @@ -653,6 +722,9 @@ def _build_summary( "ai_alert_card_learning_registry_readback_count": source_proof[ "ai_alert_card_learning_registry_readback_count" ], + "telegram_alert_ai_loop_consumer_context_readback_count": source_proof[ + "telegram_alert_ai_loop_consumer_context_readback_count" + ], "next_priority_action_id": next_priority_action["action_id"], "next_priority_work_item_id": "CIR-P0-TG-001", } diff --git a/apps/api/tests/test_ai_agent_log_controlled_writeback_consumer_readback_api.py b/apps/api/tests/test_ai_agent_log_controlled_writeback_consumer_readback_api.py index 62bd7e057..77340da4a 100644 --- a/apps/api/tests/test_ai_agent_log_controlled_writeback_consumer_readback_api.py +++ b/apps/api/tests/test_ai_agent_log_controlled_writeback_consumer_readback_api.py @@ -59,6 +59,18 @@ class _FailingContext: return False +@pytest.fixture(autouse=True) +def _stub_telegram_alert_card_readback(monkeypatch): + async def fake_alert_card_readback(**_kwargs): + return _telegram_alert_card_readback() + + monkeypatch.setattr( + consumer_module, + "list_ai_alert_card_delivery_readback", + fake_alert_card_readback, + ) + + def _ledger_rows() -> list[dict]: return [ { @@ -118,6 +130,71 @@ def _consumer_receipt_rows() -> list[dict]: ] +def _telegram_alert_card_readback() -> dict: + return { + "summary": { + "project_id": "awoooi", + "learning_registry_target_count": 6, + "learning_registry_ready_target_count": 6, + "learning_registry_missing_target_count": 0, + "learning_registry_next_action": ( + "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" + ), + }, + "items": [ + { + "message_id": "msg-telegram-1", + "run_id": "run-telegram-1", + "project_id": "awoooi", + "event_type": "wazuh_dashboard_api_readback_degraded", + "lane": "siem_observability_readback_degraded", + "learning_writeback_refs": { + "receipt_id": "msg-telegram-1", + "run_id": "run-telegram-1", + "work_item_id": "CIR-P0-TG-001", + "post_verifier_ref": ( + "verifier://awoooi/telegram-alert-delivery-receipt/" + "run-telegram-1" + ), + "ai_agent_context_ref": ( + "ai-agent://awoooi/telegram-alert-learning-context/" + "run-telegram-1/msg-telegram-1" + ), + }, + "learning_registry_bindings": [ + { + "target": target, + "status": "ready_for_consumer_context", + "consumer_surface": surface, + "ref": f"{target}://awoooi/telegram-alert/msg-telegram-1", + "work_item_id": "CIR-P0-TG-001", + "target_write_performed": False, + "raw_payload_included": False, + } + for target, surface in [ + ("km", "knowledge_memory_context"), + ("rag", "rag_index_candidate_context"), + ("playbook", "playbook_trust_candidate_context"), + ("mcp", "mcp_audit_feedback_context"), + ("verifier", "post_apply_verifier_feedback_context"), + ("ai_agent", "autonomous_runtime_decision_context"), + ] + ], + } + ], + "learning_registry": { + "schema_version": "ai_alert_card_learning_registry_readback_v1", + "status": "learning_registry_ready", + "project_id": "awoooi", + "target_count": 6, + "ready_target_count": 6, + "missing_target_count": 0, + "ready_receipt_ref_count": 6, + "consumer_context_route": "ai_loop_agent.telegram_alert_learning_context", + }, + } + + @pytest.mark.asyncio async def test_log_controlled_writeback_consumer_loader_reads_live_ledger(monkeypatch): fake_db = _FakeDb(_ledger_rows()) @@ -237,6 +314,38 @@ def _assert_consumer_readback(payload: dict): assert payload["rollups"]["mcp_consumer_binding_count"] == 1 assert payload["rollups"]["verifier_consumer_binding_count"] == 1 assert payload["rollups"]["ai_agent_consumer_binding_count"] == 1 + assert payload["rollups"]["telegram_alert_learning_context_readback_ready"] is True + assert payload["rollups"]["telegram_alert_learning_context_receipt_count"] == 6 + assert ( + payload["rollups"]["telegram_alert_learning_ai_agent_context_receipt_count"] + == 1 + ) + assert payload["rollups"]["telegram_alert_learning_ready_target_count"] == 6 + + telegram_context = payload["telegram_alert_learning_context"] + assert telegram_context["schema_version"] == ( + "telegram_alert_ai_loop_consumer_context_readback_v1" + ) + assert telegram_context["status"] == "ai_loop_agent_context_receipt_ready" + assert telegram_context["consumer_context_route"] == ( + "ai_loop_agent.telegram_alert_learning_context" + ) + assert telegram_context["next_action"] == ( + "verify_telegram_alert_learning_context_receipts_with_ai_loop_post_apply_verifier" + ) + assert telegram_context["context_receipt_count"] == 6 + assert telegram_context["ai_agent_context_receipt_count"] == 1 + ai_agent_receipt = [ + receipt + for receipt in telegram_context["context_receipts"] + if receipt["target"] == "ai_agent" + ][0] + assert ai_agent_receipt["status"] == "ready_for_ai_loop_context" + assert ai_agent_receipt["work_item_id"] == "CIR-P0-TG-001" + assert ai_agent_receipt["metadata_only"] is True + assert ai_agent_receipt["target_write_performed"] is False + assert ai_agent_receipt["check_mode"]["enabled"] is True + assert ai_agent_receipt["rollback"]["required"] is True target_rollups = {item["target"]: item for item in payload["target_rollups"]} assert set(target_rollups) == {"km", "rag", "playbook", "mcp", "verifier", "ai_agent"} @@ -264,6 +373,7 @@ def _assert_consumer_readback(payload: dict): boundaries = payload["operation_boundaries"] assert boundaries["consumer_readback_only"] is True assert boundaries["metadata_ledger_read_performed"] is True + assert boundaries["telegram_alert_registry_metadata_read_performed"] is True assert boundaries["km_write_performed"] is False assert boundaries["rag_index_write_performed"] is False assert boundaries["playbook_trust_write_performed"] is False diff --git a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py index 750e8286d..310f4472f 100644 --- a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py +++ b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py @@ -32,7 +32,7 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): } summary = payload["summary"] - assert summary["telegram_alert_surface_count"] == 6 + assert summary["telegram_alert_surface_count"] == 7 assert summary["known_direct_send_gap_count"] == 0 assert summary["known_direct_send_gap_file_count"] == 0 assert summary["workflow_direct_send_gap_count"] == 0 @@ -54,8 +54,9 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert summary["ai_alert_card_delivery_readback_endpoint_count"] == 1 assert summary["ai_alert_card_learning_writeback_refs_count"] == 1 assert summary["ai_alert_card_learning_registry_readback_count"] == 1 + assert summary["telegram_alert_ai_loop_consumer_context_readback_count"] == 1 assert summary["next_priority_action_id"] == ( - "consume_telegram_alert_learning_registry_in_ai_loop_agent_context" + "verify_telegram_alert_learning_context_receipts_with_ai_loop_post_apply_verifier" ) matrix = {item["surface_id"]: item for item in payload["matrix"]} @@ -88,6 +89,12 @@ def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): assert matrix["telegram_alert_learning_registry_readback"]["learning_writeback"] == ( "ready_km_rag_playbook_mcp_verifier_ai_agent_registry" ) + assert matrix["telegram_ai_loop_agent_consumer_context"]["status"] == ( + "ai_loop_context_readback_present" + ) + assert matrix["telegram_ai_loop_agent_consumer_context"]["ai_route"] == ( + "ready_ai_loop_agent_context_reuse" + ) assert payload["direct_send_gap_refs"] == [] assert not any( @@ -131,6 +138,10 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): "ai_agent_context_ref\n", encoding="utf-8", ) + (tmp_path / "src/services/ai_agent_log_controlled_writeback_consumer_readback.py").write_text( + "telegram_alert_ai_loop_consumer_context_readback_v1\n", + encoding="utf-8", + ) proof = _inspect_source_contract(tmp_path) @@ -145,6 +156,8 @@ def test_telegram_matrix_source_contract_supports_container_layout(tmp_path): assert proof["ai_alert_card_learning_registry_readback_present"] is True assert proof["ai_alert_card_learning_registry_readback_count"] == 1 assert proof["ai_alert_card_ai_agent_context_ref_present"] is True + assert proof["telegram_alert_ai_loop_consumer_context_readback_present"] is True + assert proof["telegram_alert_ai_loop_consumer_context_readback_count"] == 1 def test_telegram_alert_ai_automation_matrix_endpoint_returns_readback(): diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 1fb126df7..73f59ecb5 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -11803,6 +11803,8 @@ "targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent", "contextWrites": "Context receipts", "contextWritesDetail": "target writeback receipts", + "telegramContext": "Telegram AI context", + "telegramContextDetail": "registry targets {targets}", "blockers": "Active blockers", "noBlockers": "無 active blocker", "targetDetail": "context writes / bindings", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 85b06105a..8ac074f47 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -11803,6 +11803,8 @@ "targetsDetail": "KM / RAG / PlayBook / MCP / Verifier / AI Agent", "contextWrites": "Context receipts", "contextWritesDetail": "target writeback receipts", + "telegramContext": "Telegram AI context", + "telegramContextDetail": "registry targets {targets}", "blockers": "Active blockers", "noBlockers": "無 active blocker", "targetDetail": "context writes / bindings", diff --git a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx index 040824c76..740be16c9 100644 --- a/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx +++ b/apps/web/src/components/awooop/autonomous-runtime-receipt-panel.tsx @@ -238,6 +238,11 @@ type LogConsumerReadbackPayload = { mcp_context_receipt_write_count?: number | null; verifier_context_receipt_write_count?: number | null; ai_agent_context_receipt_write_count?: number | null; + telegram_alert_learning_context_readback_ready?: boolean | null; + telegram_alert_learning_context_receipt_count?: number | null; + telegram_alert_learning_ai_agent_context_receipt_count?: number | null; + telegram_alert_learning_ready_target_count?: number | null; + telegram_alert_learning_target_count?: number | null; } | null; operation_boundaries?: { runtime_target_write_performed?: boolean | null; @@ -803,6 +808,28 @@ export function AutonomousRuntimeReceiptPanel({ ? "neutral" as Tone : runtimeTargetWritePerformed ? "ok" as Tone : "neutral" as Tone, }, + { + key: "telegramContext", + label: t("writeback.telegramContext"), + value: readbackRatio( + consumerRollups.telegram_alert_learning_ai_agent_context_receipt_count, + consumerRollups.telegram_alert_learning_context_receipt_count, + hasConsumerReadback + ), + detail: t("writeback.telegramContextDetail", { + targets: readbackRatio( + consumerRollups.telegram_alert_learning_ready_target_count, + consumerRollups.telegram_alert_learning_target_count, + hasConsumerReadback + ), + }), + icon: Rocket, + tone: !hasConsumerReadback + ? "neutral" as Tone + : consumerRollups.telegram_alert_learning_context_readback_ready === true + ? "ok" as Tone + : "warn" as Tone, + }, { key: "blockers", label: t("writeback.blockers"), diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index acee2e1d5..e3633f332 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-07-02 — 20:42 CIR-P0-TG-001 Telegram alert AI Loop consumer context receipt + +**完成內容**: +- `agent-log-controlled-writeback-consumer-readback` 已新增 `telegram_alert_learning_context`,從 `/api/v1/platform/runs/ai-alert-cards` 讀取 Telegram alert learning registry,展平成 AI Loop Agent 可消費的 metadata-only context receipts。 +- 新 receipt 覆蓋 KM、RAG、PlayBook、MCP、verifier、AI Agent 六個 target,保留 target selector、source-of-truth diff、check-mode、rollback、post-verifier refs;不寫 KM / RAG index / PlayBook trust、不呼叫 MCP、不發 Telegram、不存 raw payload。 +- `telegram_alert_ai_automation_matrix` 已新增 `telegram_ai_loop_agent_consumer_context` surface;direct send gap 維持 0 後,下一個 P0 action 推進為 `verify_telegram_alert_learning_context_receipts_with_ai_loop_post_apply_verifier`。 +- `awoooi-priority-work-order-readback` 與 AI automation node receipts 已接上 Telegram context receipt rollups;前端 AI automation receipt panel 新增 Telegram AI context 卡片。 + +**驗證**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_ai_agent_log_controlled_writeback_consumer_readback_api.py apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/tests/test_awooop_operator_timeline_labels.py -q -p no:cacheprovider`:`90 passed`。 +- `python3.11 -m py_compile apps/api/src/services/ai_agent_log_controlled_writeback_consumer_readback.py apps/api/src/services/telegram_alert_ai_automation_matrix.py apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/src/services/platform_operator_service.py apps/api/src/api/v1/agents.py`:通過。 +- `python3.11 -m json.tool apps/web/messages/zh-TW.json`、`python3.11 -m json.tool apps/web/messages/en.json`、`pnpm --dir apps/web typecheck`、`git diff --check`:通過。 + +**仍維持**: +- 未送 Telegram、未呼叫 Bot API、未讀 secret / token / `.env` / raw sessions / SQLite / auth;未寫 production DB、未改 receiver route、未 workflow_dispatch、未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall。 + ## 2026-07-02 — 20:42 P0-006 Alertmanager webhook request counter exposure gap **完成內容**: diff --git a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md index 9f0059c6d..d5fc44114 100644 --- a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md +++ b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md @@ -88,7 +88,7 @@ | 2 | CIR-P0-AILOOP-002 | P0 | 「所有 AI 自動化完整流程、節點必須完整且詳盡紀錄」 | 每次 AI 自動化事件記錄 trace_id、run_id、work_item_id、節點狀態、timestamp、evidence refs、verifier、rollback、KM/PlayBook trust writeback | 已升為 API/UI 可見 P0 工作項;節點 schema 待補 | 建立 AI automation node receipt schema,先套用 Telegram alert 與 P0 runtime lanes | | 3 | CIR-P0-LOG-001 | P0 | 「所有專案、產品、網站、服務、套件、工具的 LOG 都要收進去」 | 建立全域 LOG source registry 與 ingestion adapters,含 redaction、retention、freshness、last_success、owner lane | 已升為 API/UI 可見 P0 工作項 | 先做 metadata-only LOG source registry,不讀 secret / raw payload | | 4 | CIR-P0-LOG-002 | P0 | 「LOG 要貼標、分類、分群,讓 AI Agent 可以處理」 | LOG 自動貼 product/project/site/service/package/tool、severity、lane、confidence、AI route、cluster_id、fingerprint | 已升為 API/UI 可見 P0 工作項 | 定義 tag taxonomy 與 clustering readback,先覆蓋 Telegram、CPU、backup、freshness | -| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow / ops / API direct send 已收斂到 0;`/runs/ai-alert-cards` 已補 KM / PlayBook / RAG / MCP `learning_writeback_refs` read model,並投影 KM / PlayBook / RAG / MCP / verifier / AI Agent registry bindings | 接 AI Loop Agent consumer context receipt,人工預設只允許 critical / break-glass 或歷史證據 | +| 5 | CIR-P0-TG-001 | P0 | 「所有發送到 Telegram 的監控告警是否全面清查、完整寫入 DB 或日誌、符合 AI 自動化?」 | 每個 Telegram 告警映射 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback;direct send / manual default 列缺口 | workflow / ops / API direct send 已收斂到 0;`/runs/ai-alert-cards` 已補 KM / PlayBook / RAG / MCP `learning_writeback_refs` read model,已投影 KM / PlayBook / RAG / MCP / verifier / AI Agent registry bindings,並接進 AI Loop Agent consumer context receipt | 接 post-apply verifier receipt,人工預設只允許 critical / break-glass 或歷史證據 | | 6 | CIR-P0-IA-001 | P0 | 「IwoooS 被移除、導航列菜單被移除,且沒有整合到相關頁面」 | 建立 IA/nav removal recovery ledger:被移除 menu/route/page 必須有保留、合併、替代目的地、redirect、權限邊界與 smoke | 已升為 API/UI 可見 P0 工作項;route/menu 盤點待補 | 盤點 navigation config、route list 與 removed-page destination,補 readback | | 7 | CIR-P1-AUTO-001 | P1 | 「不能偏離不代表一定要每一個工作完成,才能進行下一個」 | 建立主線並行推進規則:不離開 P0/P1 主線,但依賴允許時可同時推 API/UI、LOG、Telegram、KM/RAG/MCP/PlayBook、verifier | 已升為 API/UI 可見 P1 工作項 | 後續回報以 active lane + evidence 更新,不等待單一項完全結束 | | 8 | CIR-P1-UI-001 | P1 | 「UI/UX 工作項目不見;整個前端頁面一大堆文字,要主流專業產品做法」 | UI/UX 成為主線:first-viewport cockpit、cards/flow rows、少文字牆、route/nav 一致、desktop/mobile smoke | 已升為 API/UI spotlight | 先讓 AI Loop / LOG / Telegram / IA 工作項在 Work Items spotlight 可見,再排 UI smoke | @@ -120,7 +120,7 @@ | AI 自動化流程 / 節點紀錄 | 已升為 P0 工作項;trace/run/work_item/node/evidence/verifier/writeback schema 待補 | 建立 AI automation node receipt schema | | 全域 LOG 收集 | 已升為 P0 工作項;source registry / adapter / freshness / retention 尚未統一 | 建立 metadata-only LOG source registry | | LOG 貼標 / 分類 / 分群 | 已升為 P0 工作項;tag taxonomy / cluster readback 待補 | 先覆蓋 Telegram、CPU、backup、freshness | -| Telegram 告警 AI 自動化 | 已升為 P0 工作項;direct send / DB receipt / AI route / controlled queue gap 需全面 readback | 擴充 Telegram alert inventory/readback | +| Telegram 告警 AI 自動化 | direct send / DB receipt / AI route / controlled queue / learning registry / AI Loop consumer context 已有 readback | 接 post-apply verifier receipt,並把結果回寫 AI Loop 節點 | | IwoooS / 導航 IA 復原 | 已升為 P0 工作項;removed menu/route/page destination 待盤點 | 盤點 navigation config、route list、removed-page integration | | 主線並行推進規則 | 已升為 P1 工作項;需在回報與 UI 顯示 active lanes | 後續以 active lane + evidence 更新,不因單一 blocker 停全局 | | UI/UX 專業化主線 | 已升為 P1 工作項;Work Items spotlight 先呈現核心缺口 | 排 AwoooP / Approvals / Runs / Work Items / Alerts desktop/mobile smoke |