From 88475d1cde3f0a0e15f82cb0f89bcfc2f7f9d8d0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 7 May 2026 15:18:19 +0100 Subject: [PATCH] Make Patrol runtime failures actionable Refs #1463 --- .../v6/internal/subsystems/ai-runtime.md | 6 + .../v6/internal/subsystems/api-contracts.md | 11 ++ .../subsystems/patrol-intelligence.md | 6 + .../src/api/__tests__/patrol.test.ts | 4 + frontend-modern/src/api/patrol.ts | 2 + .../src/components/patrol/RunHistoryEntry.tsx | 17 ++ .../patrol/__tests__/RunHistoryEntry.test.tsx | 24 +++ internal/ai/patrol.go | 8 + internal/ai/patrol_history_persistence.go | 4 + .../ai/patrol_history_persistence_test.go | 8 + internal/ai/patrol_run.go | 59 +++--- internal/ai/patrol_runtime_failure.go | 126 +++++++++++++ internal/ai/patrol_runtime_failure_test.go | 177 ++++++++++++++++++ internal/config/persistence.go | 8 + internal/config/persistence_extended_test.go | 16 ++ .../tests/18-patrol-runtime-state.spec.ts | 10 + 16 files changed, 450 insertions(+), 36 deletions(-) create mode 100644 internal/ai/patrol_runtime_failure.go create mode 100644 internal/ai/patrol_runtime_failure_test.go diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 174dd029c..7818f8c89 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -98,6 +98,12 @@ runtime cost control, and shared AI transport surfaces. 1. Update this contract when canonical AI runtime or transport entry points move 2. Keep AI runtime and shared API proof routing aligned in `registry.json` 3. Preserve explicit coverage for chat, Patrol, remediation, and cost-control behavior when AI runtime changes + Patrol runtime failures are part of that runtime contract: provider, model, + tool-calling, auth, quota, rate-limit, context-window, and connectivity + failures must be classified in `internal/ai/` before they reach operators, + surfaced as the synthetic Patrol runtime finding, and preserved on patrol + run records as structured error summary/detail instead of collapsing to + generic analysis-failed copy. 4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops 5. Preserve auditability for outbound model-bound context exports and keep the export record aligned with the prompt boundary that actually reaches the provider External provider-bound unified-resource context must enforce the same diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index f7f8f84bf..b4c8334ae 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -926,6 +926,11 @@ the canonical monitored-system blocked payload. rather than inventing a second finding-context transport shape. 7. Keep Patrol summary payload consumers aligned on one assessment hierarchy: transport-driven Patrol summary surfaces may show supporting counts and outcomes, but the canonical assessment and verification states must remain singular and not be repeated as a second compact verdict strip 8. Keep Patrol verification and activity facts unified on one transport-backed secondary status area: when frontend consumers combine Patrol status payloads (`runtime_state`, `last_patrol_at`, `last_activity_at`, `trigger_status`) with run-history transport, the latest run result, activity mix, scoped-trigger state, and circuit-breaker context must read as one supporting explanation beneath the primary assessment instead of being re-expanded into a separate full-width status strip plus duplicate summary layers + and the Patrol runtime-failure run-history contract, so backend payloads, + persistence adapters, and `frontend-modern/src/api/patrol.ts` preserve + `error_summary` and `error_detail` whenever an erroring run has structured + provider, model, tool, context-window, quota, auth, rate-limit, or + connectivity failure context and the main Patrol page composition boundary, so once that governed secondary area exists inside the summary shell the same payloads must not also drive a second page-level status strip elsewhere on the route @@ -3103,6 +3108,12 @@ That same frontend run-history path must also preserve and expose `triage_flags` and `triage_skipped_llm` from canonical patrol run records so deterministic triage-only runs do not collapse into generic "no analysis" history entries. +Patrol run-history payloads must also preserve structured runtime failure +context. When a Patrol run records `error_count > 0`, the backend may include +`error_summary` and `error_detail`; persistence, API responses, and +`frontend-modern/src/api/patrol.ts` must preserve those fields so the Patrol UI +can explain provider/model/tool runtime failures without scraping finding +copy or inferring meaning from a generic error status. Patrol status payloads no longer carry quickstart credit state as an ordinary v6 GA API contract. `quickstart_credits_remaining`, `quickstart_credits_total`, and `using_quickstart` are retired public fields; diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index db011e0dc..d2cd12486 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -430,6 +430,12 @@ the summary card and the findings list. The active finding row should surface the same Patrol runtime classification with a runtime-qualified severity badge such as `Runtime issue` or `Runtime critical`, rather than pairing a generic infrastructure severity chip like `warning` with a second Patrol-runtime +label. +Patrol run history must follow the same operator-facing runtime failure +contract. Expanded erroring runs should show the backend-provided +`error_summary` and bounded `error_detail` near the run narrative, so a +provider/model/tool failure remains actionable even when the operator starts +from the Runs tab rather than the synthetic runtime finding. classification badge. That same shared finding-presentation helper should also own Patrol finding subject labels, so Patrol-owned synthetic service findings render as diff --git a/frontend-modern/src/api/__tests__/patrol.test.ts b/frontend-modern/src/api/__tests__/patrol.test.ts index c3b8feba2..1d62eafbf 100644 --- a/frontend-modern/src/api/__tests__/patrol.test.ts +++ b/frontend-modern/src/api/__tests__/patrol.test.ts @@ -129,6 +129,8 @@ describe('patrol api', () => { finding_ids: [], error_count: 0, status: 'healthy', + error_summary: 'Selected model does not support Patrol tools', + error_detail: 'provider rejected tool_choice', triage_flags: 2, triage_skipped_llm: true, tool_call_count: 0, @@ -143,6 +145,8 @@ describe('patrol api', () => { alertIdentifier: 'canonical-alert-1', effective_scope_resource_ids: ['resource-1', 'resource-2'], truenas_checked: 1, + error_summary: 'Selected model does not support Patrol tools', + error_detail: 'provider rejected tool_choice', triage_flags: 2, triage_skipped_llm: true, }); diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index b172a32f3..232c61431 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -429,6 +429,8 @@ export interface PatrolRunRecord { finding_ids?: string[]; error_count: number; status: PatrolRunStatus; + error_summary?: string; + error_detail?: string; triage_flags: number; triage_skipped_llm?: boolean; ai_analysis?: string; diff --git a/frontend-modern/src/components/patrol/RunHistoryEntry.tsx b/frontend-modern/src/components/patrol/RunHistoryEntry.tsx index 5fbab570e..e4b070220 100644 --- a/frontend-modern/src/components/patrol/RunHistoryEntry.tsx +++ b/frontend-modern/src/components/patrol/RunHistoryEntry.tsx @@ -153,6 +153,9 @@ export function RunHistoryEntry(props: RunHistoryEntryProps) { const coverageSummary = getPatrolRunCoverageSummary(run); const hasFindingsSnapshot = run.finding_ids !== undefined; const runStatus = getPatrolRunStatusPresentation(run.status, run.error_count, hasFindingsSnapshot); + const runErrorSummary = String(run.error_summary || '').trim(); + const runErrorDetail = String(run.error_detail || '').trim(); + const hasRunErrorDetail = run.error_count > 0 && (runErrorSummary || runErrorDetail); return (
+ +
+ +
+

{runErrorSummary || 'Patrol analysis did not complete'}

+ +

+ {runErrorDetail} +

+
+
+
+
+ {/* Section 2: Resources Scanned */} 0}>
diff --git a/frontend-modern/src/components/patrol/__tests__/RunHistoryEntry.test.tsx b/frontend-modern/src/components/patrol/__tests__/RunHistoryEntry.test.tsx index 7062f0d23..3c5178ae0 100644 --- a/frontend-modern/src/components/patrol/__tests__/RunHistoryEntry.test.tsx +++ b/frontend-modern/src/components/patrol/__tests__/RunHistoryEntry.test.tsx @@ -195,6 +195,30 @@ describe('RunHistoryEntry', () => { ).toBeInTheDocument(); }); + it('surfaces structured Patrol runtime error details on expanded runs', () => { + render(() => ( + + )); + + expect(screen.getByText('Selected model does not support Patrol tools')).toBeInTheDocument(); + expect(screen.getByText(/tool_choice/)).toBeInTheDocument(); + expect(screen.getByText('error')).toBeInTheDocument(); + }); + it('keeps zero-coverage scoped runs on the shared coverage narrative', () => { render(() => ( { ), ).toBeVisible(); await expect(page.getByText("Last full patrol")).toBeVisible(); + + await page.getByRole("button", { name: "Runs" }).click(); + await page.getByRole("button", { name: /Alert fired/i }).click(); + await expect( + page.getByText("Selected model does not support Patrol tools"), + ).toBeVisible(); + await expect(page.getByText(/tool_choice/)).toBeVisible(); }); });