From 873c7aa388997058519db0e96afc1da2e435b830 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 18:05:38 +0800 Subject: [PATCH] feat(platform): surface canonical run correlation --- .../src/services/platform_operator_service.py | 64 +++++++++++-------- .../test_awooop_operator_timeline_labels.py | 41 ++++++++++++ apps/web/messages/en.json | 16 +++++ apps/web/messages/zh-TW.json | 16 +++++ .../[locale]/awooop/runs/[run_id]/page.tsx | 12 +++- .../web/src/app/[locale]/awooop/runs/page.tsx | 41 ++++++++++-- 6 files changed, 156 insertions(+), 34 deletions(-) diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index 57cc322be..17da9cf9d 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -1075,6 +1075,16 @@ async def list_contracts( # ============================================================================= +def _run_identity_readback(run: AwoooPRunState) -> dict[str, str | None]: + """Return the public-safe correlation identity stored for one run.""" + + return correlation_readback_for_run( + run.project_id, + run.run_id, + run.trace_id, + ) + + def _run_list_item( run: AwoooPRunState, *, @@ -1089,11 +1099,7 @@ def _run_list_item( "agent_id": run.agent_id, "state": run.state, "is_shadow": run.is_shadow, - **correlation_readback_for_run( - run.project_id, - run.run_id, - run.trace_id, - ), + **_run_identity_readback(run), "cost_usd": run.cost_usd, "step_count": run.step_count, "created_at": run.created_at, @@ -1103,6 +1109,32 @@ def _run_list_item( } +def _run_detail_payload(run: AwoooPRunState) -> dict[str, Any]: + """Build detail readback without diverging from list correlation truth.""" + + return { + "run_id": run.run_id, + "project_id": run.project_id, + "agent_id": run.agent_id, + "state": run.state, + "is_shadow": run.is_shadow, + **_run_identity_readback(run), + "trigger_type": run.trigger_type, + "trigger_ref": run.trigger_ref, + "cost_usd": run.cost_usd, + "step_count": run.step_count, + "attempt_count": run.attempt_count, + "max_attempts": run.max_attempts, + "error_code": run.error_code, + "error_detail": run.error_detail, + "created_at": run.created_at, + "started_at": run.started_at, + "completed_at": run.completed_at, + "timeout_at": run.timeout_at, + "heartbeat_at": run.heartbeat_at, + } + + async def list_runs( project_id: str | None, state: str | None, @@ -8623,27 +8655,7 @@ async def get_run_detail( ) mcp_calls = list(mcp_result.scalars().all()) - run_payload = { - "run_id": run.run_id, - "project_id": run.project_id, - "agent_id": run.agent_id, - "state": run.state, - "is_shadow": run.is_shadow, - "trace_id": run.trace_id, - "trigger_type": run.trigger_type, - "trigger_ref": run.trigger_ref, - "cost_usd": run.cost_usd, - "step_count": run.step_count, - "attempt_count": run.attempt_count, - "max_attempts": run.max_attempts, - "error_code": run.error_code, - "error_detail": run.error_detail, - "created_at": run.created_at, - "started_at": run.started_at, - "completed_at": run.completed_at, - "timeout_at": run.timeout_at, - "heartbeat_at": run.heartbeat_at, - } + run_payload = _run_detail_payload(run) step_items = [ { diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index 5528bfd9b..aa2fd6870 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -56,6 +56,7 @@ from src.services.platform_operator_service import ( _remediation_timeline_summary, _repair_candidate_projection_from_metadata, _run_callback_reply_summary, + _run_detail_payload, _run_list_item, _run_remediation_list_summary, _score_source_correlation_event, @@ -618,6 +619,46 @@ def test_run_list_item_preserves_noncanonical_trace_truth( assert item["correlation_status"] == "legacy_or_invalid_trace" +@pytest.mark.parametrize("trace_id", [canonical_traceparent(UUID("019f88d5-f6c9-7a18-b764-df0df0cbe222")), "legacy-trace", None]) +def test_run_list_and_detail_share_correlation_readback( + trace_id: str | None, +) -> None: + run_id = UUID("019f88d5-f6c9-7a18-b764-df0df0cbe222") + run = SimpleNamespace( + run_id=run_id, + project_id="awoooi", + agent_id="legacy-signal-worker", + state="completed", + is_shadow=True, + trace_id=trace_id, + trigger_type="channel_event", + trigger_ref="event-1", + cost_usd=Decimal("0.0000"), + step_count=4, + attempt_count=1, + max_attempts=3, + error_code=None, + error_detail=None, + created_at=datetime(2026, 7, 22, 8, 0, 0), + started_at=datetime(2026, 7, 22, 8, 0, 1), + completed_at=datetime(2026, 7, 22, 8, 0, 2), + timeout_at=None, + heartbeat_at=datetime(2026, 7, 22, 8, 0, 2), + ) + + list_item = _run_list_item( + run, + remediation_summary=None, + callback_reply_summary=None, + ) + detail = _run_detail_payload(run) + + keys = ("trace_id", "work_item_id", "correlation_status") + assert {key: list_item[key] for key in keys} == { + key: detail[key] for key in keys + } + + def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") message_id = UUID("56cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 539ad8ba5..36c638ecd 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -11253,6 +11253,16 @@ "listEvidence": { "column": "AI 證據", "callbackColumn": "TG Callback", + "correlation": { + "column": "Run / correlation", + "workItemMissing": "Canonical work item unavailable", + "traceMissing": "Trace unavailable", + "statuses": { + "canonical": "Canonical", + "legacy_or_invalid_trace": "Legacy trace", + "missing": "Awaiting API readback" + } + }, "sourceFlow": { "column": "來源流程", "notLinked": "尚未關聯 incident", @@ -13757,7 +13767,13 @@ "title": "Run 摘要", "project": "Project", "agent": "Agent", + "workItem": "Work item", "traceId": "追蹤 ID", + "correlation": "Correlation status", + "correlationStatuses": { + "canonical": "Canonical: trace, run, and work item share one root", + "legacy_or_invalid_trace": "Legacy or invalid trace: stored value preserved, not claimed canonical" + }, "trigger": "觸發來源", "triggerRef": "觸發參照", "cost": "Cost", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 60818f432..4dac96a1d 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -11253,6 +11253,16 @@ "listEvidence": { "column": "AI 證據", "callbackColumn": "TG Callback", + "correlation": { + "column": "Run / 關聯", + "workItemMissing": "尚無 canonical work item", + "traceMissing": "尚無 trace", + "statuses": { + "canonical": "Canonical", + "legacy_or_invalid_trace": "Legacy trace", + "missing": "待 API 讀回" + } + }, "sourceFlow": { "column": "來源流程", "notLinked": "尚未關聯 incident", @@ -13757,7 +13767,13 @@ "title": "Run 摘要", "project": "Project", "agent": "Agent", + "workItem": "Work item", "traceId": "追蹤 ID", + "correlation": "關聯狀態", + "correlationStatuses": { + "canonical": "Canonical:trace、run 與 work item 同源", + "legacy_or_invalid_trace": "Legacy / 無效 trace:保留原值,未宣稱 canonical" + }, "trigger": "觸發來源", "triggerRef": "觸發參照", "cost": "Cost", diff --git a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx index a1578900e..ee27a1fab 100644 --- a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx @@ -52,6 +52,8 @@ interface RunDetail { state: string; is_shadow: boolean; trace_id?: string | null; + work_item_id?: string | null; + correlation_status?: "canonical" | "legacy_or_invalid_trace" | null; trigger_type?: string | null; trigger_ref?: string | null; cost_usd: number | string; @@ -1522,7 +1524,15 @@ export default function RunDetailPage({
- + + + diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index fb3c9c9b8..cc8655364 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -288,6 +288,9 @@ interface Run { agent_id: string; state: RunState; is_shadow: boolean; + trace_id?: string | null; + work_item_id?: string | null; + correlation_status?: "canonical" | "legacy_or_invalid_trace" | null; cost_usd: number | string; step_count: number; created_at: string; @@ -2030,6 +2033,7 @@ function RunRow({ statusChain?: AwoooPStatusChain | null; sourceFlowLoading: boolean; }) { + const tCorrelation = useTranslations("awooop.listEvidence.correlation"); const formattedDate = run.created_at ? new Date(run.created_at).toLocaleDateString("zh-TW", { month: "2-digit", @@ -2041,16 +2045,39 @@ function RunRow({ const cost = Number(run.cost_usd ?? 0); const incidentIds = linkedIncidentIds(run.remediation_summary); + const correlationStatus = run.correlation_status ?? "missing"; + const correlationTone = correlationStatus === "canonical" + ? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]" + : "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; return ( - - {run.run_id.slice(0, 8)} - +
+ + {run.run_id.slice(0, 8)} + +

+ {run.work_item_id ?? tCorrelation("workItemMissing")} +

+
+ + {tCorrelation(`statuses.${correlationStatus}` as never)} + + + {run.trace_id ?? tCorrelation("traceMissing")} + +
+
@@ -5481,7 +5508,7 @@ export default function RunsPage() { - Run ID + {tEvidence("correlation.column")} Project ID