feat(telegram): persist callback owner review snapshots
This commit is contained in:
@@ -96,6 +96,7 @@ class CallbackReplyItem(BaseModel):
|
||||
callback_reply: dict[str, Any]
|
||||
awooop_status_chain: dict[str, Any] | None = None
|
||||
km_stale_completion_summary: dict[str, Any] | None = None
|
||||
persisted_km_stale_completion_summary: dict[str, Any] | None = None
|
||||
run_detail_href: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -374,6 +374,8 @@ async def list_callback_replies(
|
||||
m.sent_at,
|
||||
m.triggered_by_state,
|
||||
m.source_envelope -> 'callback_reply' AS callback_reply,
|
||||
m.source_envelope -> 'km_stale_completion_summary'
|
||||
AS persisted_km_stale_completion_summary,
|
||||
r.agent_id,
|
||||
r.state AS run_state,
|
||||
r.created_at AS run_created_at
|
||||
@@ -1053,6 +1055,9 @@ def _callback_reply_event_item(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"agent_id": row.get("agent_id"),
|
||||
"run_created_at": row.get("run_created_at"),
|
||||
"callback_reply": callback_reply,
|
||||
"persisted_km_stale_completion_summary": _as_dict(
|
||||
row.get("persisted_km_stale_completion_summary"),
|
||||
) or None,
|
||||
"run_detail_href": (
|
||||
f"/awooop/runs/{run_id}?project_id={project_id}"
|
||||
if run_id and project_id
|
||||
|
||||
@@ -824,6 +824,7 @@ def _callback_reply_source_envelope_extra(
|
||||
callback_action: str | None = None,
|
||||
parse_mode: str | None = None,
|
||||
error: str | None = None,
|
||||
km_stale_completion_summary: dict[str, object] | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
"""Build AwoooP metadata for Telegram detail/history callback replies."""
|
||||
if not incident_id:
|
||||
@@ -844,12 +845,68 @@ def _callback_reply_source_envelope_extra(
|
||||
if error:
|
||||
callback_reply["error"] = _sanitize_telegram_error(error)[:300]
|
||||
|
||||
return {
|
||||
extra: dict[str, object] = {
|
||||
"callback_reply": callback_reply,
|
||||
"source_refs": {
|
||||
"incident_ids": [incident_id],
|
||||
},
|
||||
}
|
||||
km_snapshot = _callback_reply_km_stale_completion_snapshot(
|
||||
km_stale_completion_summary,
|
||||
)
|
||||
if km_snapshot:
|
||||
extra["km_stale_completion_summary"] = km_snapshot
|
||||
|
||||
return extra
|
||||
|
||||
|
||||
def _callback_reply_km_stale_completion_snapshot(
|
||||
summary: dict[str, object] | None,
|
||||
) -> dict[str, object] | None:
|
||||
"""Persist a compact KM owner-review state snapshot with callback evidence."""
|
||||
if not isinstance(summary, dict):
|
||||
return None
|
||||
|
||||
snapshot: dict[str, object] = {
|
||||
"schema_version": "km_stale_owner_review_callback_reply_snapshot_v1",
|
||||
"source_schema_version": str(summary.get("schema_version") or ""),
|
||||
"status": str(summary.get("status") or "observed"),
|
||||
"incident_id": str(summary.get("incident_id") or ""),
|
||||
"project_id": str(summary.get("project_id") or ""),
|
||||
"missing_reason": str(summary.get("missing_reason") or ""),
|
||||
"ready_count": _safe_int(summary.get("ready_count")),
|
||||
"blocked_count": _safe_int(summary.get("blocked_count")),
|
||||
"completed_count": _safe_int(summary.get("completed_count")),
|
||||
"failed_count": _safe_int(summary.get("failed_count")),
|
||||
"related_total": _safe_int(summary.get("related_total")),
|
||||
"writes_on_read": bool(summary.get("writes_on_read")),
|
||||
"batch_writes_allowed": bool(summary.get("batch_writes_allowed")),
|
||||
"manual_review_required": bool(summary.get("manual_review_required", True)),
|
||||
}
|
||||
triage = _km_stale_completion_triage(summary)
|
||||
if triage:
|
||||
raw_supporting_agents = triage.get("supporting_agents")
|
||||
supporting_agents = (
|
||||
raw_supporting_agents
|
||||
if isinstance(raw_supporting_agents, list)
|
||||
else []
|
||||
)
|
||||
snapshot["triage"] = {
|
||||
"schema_version": str(triage.get("schema_version") or ""),
|
||||
"flow_stage": str(triage.get("flow_stage") or ""),
|
||||
"ai_lead_agent": str(triage.get("ai_lead_agent") or ""),
|
||||
"supporting_agents": [
|
||||
str(agent)
|
||||
for agent in supporting_agents
|
||||
if str(agent).strip()
|
||||
][:5],
|
||||
"automation_state": str(triage.get("automation_state") or ""),
|
||||
"safe_to_auto_repair": bool(triage.get("safe_to_auto_repair")),
|
||||
"blocking_reason": str(triage.get("blocking_reason") or ""),
|
||||
"matching_strategy": str(triage.get("matching_strategy") or ""),
|
||||
}
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
def _merge_outbound_source_envelope_extra(
|
||||
@@ -864,6 +921,10 @@ def _merge_outbound_source_envelope_extra(
|
||||
if isinstance(callback_reply, dict):
|
||||
envelope["callback_reply"] = callback_reply
|
||||
|
||||
km_stale_completion_summary = extra.get("km_stale_completion_summary")
|
||||
if isinstance(km_stale_completion_summary, dict):
|
||||
envelope["km_stale_completion_summary"] = km_stale_completion_summary
|
||||
|
||||
extra_refs = extra.get("source_refs")
|
||||
if isinstance(extra_refs, dict):
|
||||
source_refs = envelope.setdefault("source_refs", {})
|
||||
@@ -6101,6 +6162,7 @@ class TelegramGateway:
|
||||
reply_markup=_awooop_runs_reply_markup(incident_id),
|
||||
incident_id=incident_id,
|
||||
callback_action="detail",
|
||||
km_stale_completion_summary=km_completion_summary,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -6253,6 +6315,7 @@ class TelegramGateway:
|
||||
reply_markup=_awooop_runs_reply_markup(incident_id),
|
||||
incident_id=incident_id,
|
||||
callback_action="history",
|
||||
km_stale_completion_summary=km_completion_summary,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -6491,6 +6554,7 @@ class TelegramGateway:
|
||||
reply_markup: dict | None = None,
|
||||
incident_id: str | None = None,
|
||||
callback_action: str | None = None,
|
||||
km_stale_completion_summary: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
"""Send a multi-line HTML message without cutting Telegram tags in half."""
|
||||
chunks = _telegram_html_chunks(lines)
|
||||
@@ -6510,6 +6574,7 @@ class TelegramGateway:
|
||||
chunk_count=len(chunks),
|
||||
callback_action=callback_action,
|
||||
parse_mode="HTML",
|
||||
km_stale_completion_summary=km_stale_completion_summary,
|
||||
)
|
||||
if source_extra:
|
||||
payload[_AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY] = source_extra
|
||||
@@ -6540,6 +6605,7 @@ class TelegramGateway:
|
||||
callback_action=callback_action,
|
||||
parse_mode="plain_text",
|
||||
error=str(exc),
|
||||
km_stale_completion_summary=km_stale_completion_summary,
|
||||
)
|
||||
if fallback_source_extra:
|
||||
fallback_payload[_AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY] = fallback_source_extra
|
||||
@@ -6572,6 +6638,7 @@ class TelegramGateway:
|
||||
callback_action=callback_action,
|
||||
parse_mode="plain_text",
|
||||
error=str(fallback_exc),
|
||||
km_stale_completion_summary=km_stale_completion_summary,
|
||||
)
|
||||
if rescue_source_extra:
|
||||
rescue_payload[_AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY] = rescue_source_extra
|
||||
|
||||
@@ -375,6 +375,15 @@ def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None:
|
||||
"incident_id": "INC-20260513-79ED5E",
|
||||
"error": "HTTP error: 400",
|
||||
},
|
||||
"persisted_km_stale_completion_summary": {
|
||||
"schema_version": "km_stale_owner_review_callback_reply_snapshot_v1",
|
||||
"status": "no_related_owner_review",
|
||||
"ready_count": 4,
|
||||
"triage": {
|
||||
"flow_stage": "callback_observed_owner_review_link_missing",
|
||||
"ai_lead_agent": "Hermes",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert item["status"] == "failed"
|
||||
@@ -385,6 +394,10 @@ def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None:
|
||||
assert item["run_detail_href"] == (
|
||||
"/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38?project_id=awoooi"
|
||||
)
|
||||
assert item["persisted_km_stale_completion_summary"]["ready_count"] == 4
|
||||
assert item["persisted_km_stale_completion_summary"]["triage"]["ai_lead_agent"] == (
|
||||
"Hermes"
|
||||
)
|
||||
|
||||
|
||||
def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
@@ -444,6 +457,29 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
}
|
||||
],
|
||||
},
|
||||
"persisted_km_stale_completion_summary": {
|
||||
"schema_version": (
|
||||
"km_stale_owner_review_callback_reply_snapshot_v1"
|
||||
),
|
||||
"source_schema_version": (
|
||||
"km_stale_owner_review_completion_callback_summary_v1"
|
||||
),
|
||||
"project_id": "awoooi",
|
||||
"incident_id": "INC-20260513-79ED5E",
|
||||
"status": "matched_owner_review",
|
||||
"ready_count": 3,
|
||||
"blocked_count": 1,
|
||||
"completed_count": 2,
|
||||
"failed_count": 0,
|
||||
"batch_writes_allowed": False,
|
||||
"manual_review_required": True,
|
||||
"related_total": 1,
|
||||
"triage": {
|
||||
"flow_stage": "callback_observed_owner_review_link_missing",
|
||||
"ai_lead_agent": "Hermes",
|
||||
"automation_state": "manual_owner_review_required",
|
||||
},
|
||||
},
|
||||
"run_detail_href": (
|
||||
"/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38"
|
||||
"?project_id=awoooi"
|
||||
@@ -463,6 +499,9 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
||||
)
|
||||
assert dumped["items"][0]["km_stale_completion_summary"]["ready_count"] == 3
|
||||
assert dumped["items"][0]["km_stale_completion_summary"]["related_total"] == 1
|
||||
assert dumped["items"][0]["persisted_km_stale_completion_summary"]["triage"][
|
||||
"ai_lead_agent"
|
||||
] == "Hermes"
|
||||
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
|
||||
|
||||
|
||||
|
||||
@@ -329,6 +329,16 @@ async def test_send_request_strips_awooop_callback_metadata_before_telegram_api(
|
||||
"status": "callback_reply_sent",
|
||||
"incident_id": "INC-20260513-79ED5E",
|
||||
},
|
||||
"km_stale_completion_summary": {
|
||||
"schema_version": (
|
||||
"km_stale_owner_review_callback_reply_snapshot_v1"
|
||||
),
|
||||
"status": "no_related_owner_review",
|
||||
"ready_count": 2,
|
||||
"triage": {
|
||||
"flow_stage": "callback_observed_owner_review_link_missing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -339,6 +349,9 @@ async def test_send_request_strips_awooop_callback_metadata_before_telegram_api(
|
||||
assert captured["mirror"]["source_envelope_extra"]["callback_reply"]["status"] == (
|
||||
"callback_reply_sent"
|
||||
)
|
||||
assert captured["mirror"]["source_envelope_extra"][
|
||||
"km_stale_completion_summary"
|
||||
]["ready_count"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -394,17 +407,52 @@ async def test_send_html_line_message_marks_callback_reply_evidence(monkeypatch)
|
||||
reply_markup=reply_markup,
|
||||
incident_id="INC-20260514-F85F21",
|
||||
callback_action="history",
|
||||
km_stale_completion_summary={
|
||||
"schema_version": "km_stale_owner_review_completion_callback_summary_v1",
|
||||
"project_id": "awoooi",
|
||||
"incident_id": "INC-20260514-F85F21",
|
||||
"status": "no_related_owner_review",
|
||||
"ready_count": 3,
|
||||
"blocked_count": 1,
|
||||
"completed_count": 2,
|
||||
"failed_count": 0,
|
||||
"writes_on_read": False,
|
||||
"batch_writes_allowed": False,
|
||||
"manual_review_required": True,
|
||||
"related_total": 0,
|
||||
"work_item": {
|
||||
"triage": {
|
||||
"schema_version": "km_stale_callback_owner_review_triage_v1",
|
||||
"flow_stage": "callback_observed_owner_review_link_missing",
|
||||
"ai_lead_agent": "Hermes",
|
||||
"supporting_agents": ["OpenClaw", "ElephantAlpha"],
|
||||
"automation_state": "manual_owner_review_required",
|
||||
"safe_to_auto_repair": False,
|
||||
"blocking_reason": "no_matching_completion_item",
|
||||
"matching_strategy": "related_incident_id_exact_match",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
first_extra = sent_requests[0][1]["_awooop_source_envelope_extra"]["callback_reply"]
|
||||
fallback_extra = sent_requests[1][1]["_awooop_source_envelope_extra"]["callback_reply"]
|
||||
first_source_extra = sent_requests[0][1]["_awooop_source_envelope_extra"]
|
||||
fallback_source_extra = sent_requests[1][1]["_awooop_source_envelope_extra"]
|
||||
first_extra = first_source_extra["callback_reply"]
|
||||
fallback_extra = fallback_source_extra["callback_reply"]
|
||||
|
||||
assert first_extra["status"] == "callback_reply_sent"
|
||||
assert first_extra["action"] == "history"
|
||||
assert first_extra["parse_mode"] == "HTML"
|
||||
assert first_source_extra["km_stale_completion_summary"]["ready_count"] == 3
|
||||
assert first_source_extra["km_stale_completion_summary"]["triage"]["flow_stage"] == (
|
||||
"callback_observed_owner_review_link_missing"
|
||||
)
|
||||
assert fallback_extra["status"] == "callback_reply_fallback_sent"
|
||||
assert fallback_extra["incident_id"] == "INC-20260514-F85F21"
|
||||
assert fallback_extra["parse_mode"] == "plain_text"
|
||||
assert fallback_source_extra["km_stale_completion_summary"]["triage"][
|
||||
"ai_lead_agent"
|
||||
] == "Hermes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -2805,6 +2805,10 @@
|
||||
"noRelated": "This incident has no matching owner-review completion item yet.",
|
||||
"fetchFailed": "KM owner-review summary failed to load: {reason}",
|
||||
"openWorkItem": "Open work item",
|
||||
"snapshotTitle": "Callback-time Evidence Snapshot",
|
||||
"snapshotStatus": "Snapshot status: {status}; ready {ready} / blocked {blocked} / completed {completed} / failed {failed}",
|
||||
"snapshotFlow": "Snapshot flow: {stage}; match: {strategy}",
|
||||
"snapshotAutomation": "Snapshot automation: lead {lead}; state {state}; safe auto-repair={safe}; blocker {blocker}",
|
||||
"triageFlow": "Flow: {stage}; match: {strategy}",
|
||||
"triageAgents": "Lead: {lead}; support: {support}",
|
||||
"triageAutomation": "Automation: {state}; safe auto-repair={safe}",
|
||||
|
||||
@@ -2806,6 +2806,10 @@
|
||||
"noRelated": "本 Incident 尚未對到 owner-review completion item。",
|
||||
"fetchFailed": "KM owner-review 摘要讀取失敗:{reason}",
|
||||
"openWorkItem": "開啟工作項",
|
||||
"snapshotTitle": "Callback 當下 Evidence Snapshot",
|
||||
"snapshotStatus": "當下狀態:{status};ready {ready} / blocked {blocked} / completed {completed} / failed {failed}",
|
||||
"snapshotFlow": "當下流程:{stage};匹配:{strategy}",
|
||||
"snapshotAutomation": "當下自動化:主責 {lead};狀態 {state};可安全自動修復={safe};卡點 {blocker}",
|
||||
"triageFlow": "流程:{stage};匹配:{strategy}",
|
||||
"triageAgents": "主責:{lead};協作:{support}",
|
||||
"triageAutomation": "自動化:{state};可安全自動修復={safe}",
|
||||
|
||||
@@ -328,6 +328,7 @@ interface KmStaleCallbackOwnerReviewTriage {
|
||||
|
||||
interface KmStaleCompletionSummary {
|
||||
schema_version?: string;
|
||||
source_schema_version?: string | null;
|
||||
project_id?: string | null;
|
||||
incident_id?: string | null;
|
||||
status?: string | null;
|
||||
@@ -346,6 +347,7 @@ interface KmStaleCompletionSummary {
|
||||
related_total?: number;
|
||||
related_items?: KmStaleCompletionSummaryItem[];
|
||||
work_item?: KmStaleCallbackOwnerReviewWorkItem | null;
|
||||
triage?: KmStaleCallbackOwnerReviewTriage | null;
|
||||
}
|
||||
|
||||
interface CallbackReplyEvent {
|
||||
@@ -369,6 +371,7 @@ interface CallbackReplyEvent {
|
||||
run_detail_href?: string | null;
|
||||
awooop_status_chain?: AwoooPStatusChain | null;
|
||||
km_stale_completion_summary?: KmStaleCompletionSummary | null;
|
||||
persisted_km_stale_completion_summary?: KmStaleCompletionSummary | null;
|
||||
}
|
||||
|
||||
interface CallbackRepliesResponse {
|
||||
@@ -1587,6 +1590,58 @@ function CallbackKmCompletionSummary({
|
||||
);
|
||||
}
|
||||
|
||||
function CallbackKmCompletionSnapshot({
|
||||
snapshot,
|
||||
}: {
|
||||
snapshot?: KmStaleCompletionSummary | null;
|
||||
}) {
|
||||
const t = useTranslations("awooop.callbackReply.events.kmCompletion");
|
||||
if (!snapshot) return null;
|
||||
|
||||
const statusKey = normalizeKmCompletionStatus(snapshot.status);
|
||||
const triage = snapshot.triage ?? null;
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-[#e0ddd4] pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-[#1f5b9b]" aria-hidden="true" />
|
||||
<p className="text-xs font-semibold text-[#141413]">
|
||||
{t("snapshotTitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-1 text-xs leading-5 text-[#5f5b52]">
|
||||
<p>
|
||||
{t("snapshotStatus", {
|
||||
status: t(`statuses.${statusKey}` as never),
|
||||
ready: snapshot.ready_count ?? 0,
|
||||
blocked: snapshot.blocked_count ?? 0,
|
||||
completed: snapshot.completed_count ?? 0,
|
||||
failed: snapshot.failed_count ?? 0,
|
||||
})}
|
||||
</p>
|
||||
{triage ? (
|
||||
<>
|
||||
<p>
|
||||
{t("snapshotFlow", {
|
||||
stage: triage.flow_stage ?? "--",
|
||||
strategy: triage.matching_strategy ?? "--",
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t("snapshotAutomation", {
|
||||
lead: triage.ai_lead_agent ?? "--",
|
||||
state: triage.automation_state ?? "--",
|
||||
safe: String(triage.safe_to_auto_repair ?? false),
|
||||
blocker: triage.blocking_reason ?? "--",
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CallbackReplyEvidencePanel({
|
||||
events,
|
||||
total,
|
||||
@@ -1680,6 +1735,9 @@ function CallbackReplyEvidencePanel({
|
||||
<CallbackKmCompletionSummary
|
||||
summary={event.km_stale_completion_summary}
|
||||
/>
|
||||
<CallbackKmCompletionSnapshot
|
||||
snapshot={event.persisted_km_stale_completion_summary}
|
||||
/>
|
||||
<Link
|
||||
href={runHref as never}
|
||||
className="mt-3 inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
|
||||
|
||||
Reference in New Issue
Block a user