Remove dead Patrol assessment and save-failure handoffs

buildPatrolAssessmentAssistantHandoff lost its last caller when the Patrol
summary card's Discuss handler was removed in eac7dfe9ef (2026-05-15), and
buildPatrolConfigurationFailureHandoff lost its last caller in 27ba85ddc6
(2026-06-23). Since then only their own tests called them. A TypeScript
compiler import graph rooted at src/index.tsx confirms no reachable file
imports either one. Removing them also drops PatrolInvestigationContextSummary,
their input and output types, and the private assessment, recent-change,
correlation and save-failure helpers that only they used (found with
noUnusedLocals until it reported nothing).

Rollup had already tree-shaken all of it: production builds of the parent
tree and of this tree are byte-identical across all 187 dist files, and the
bundle-size check passes. The presentation helpers the dead code imported
keep their live callers.

Their test cases go with them. Two cases covered branches the live finding
handoff still uses, so they now run through
buildPatrolAssistantFindingHandoffActions and buildPatrolAssistantFindingHandoff:
a typed pending action without a legacy approval id stays approval-bound,
and the finding context qualifies its resource with the record's node and
truncates long lines. v8 coverage of the surviving model code matches the
pre-removal baseline except one defensive guard the live caller cannot
reach. The raw-source guards pinned none of the removed code.

The Assistant drawer still restores saved patrol_assessment and
patrol_configuration_failure sessions, so that compatibility path stays.

The contracts described both handoffs as live producers, and described a
Patrol summary card, assessment strip, assessment readout, score chip and
header trust line that the page no longer renders. patrol-intelligence,
ai-runtime, api-contracts, frontend-primitives, security-privacy,
storage-recovery, agent-lifecycle and unified-resources now say what the
code does: those handoffs survive only as saved sessions,
the drawer, session list and API keep restoring them, rules for a returning
summary card, strip or readout are conditional, and handoff_actions and
adjacent-boundary clauses name the finding and attention handoffs that send
them today. The run-history handoff clauses also claimed the browser sends a
[Patrol Run Context] block and scoped resources; it sends the briefing and
run identity, and internal/ai/patrol_assistant_handoff.go rebuilds the rest
from the run ID.

docs/AI.md (and its public mirror) told users the legacy operational score
sits in a collapsed supporting-context section. That section and the score
are gone; the section now points at Activity > Finding options and history,
where check history lives. ai_runtime_docs_policy_test.py, its registered
proof, now pins that location and rejects the retired claim. The same file
had failed on main since 2789a15fe5 reworded the Patrol mode API sentence to
"The API retains `autonomy_level` for compatibility" without updating its
pin; the pin now matches that wording.
This commit is contained in:
rcourtman
2026-10-07 15:45:49 +01:00
parent d67bef7519
commit cb5ffc2efe
16 changed files with 290 additions and 2688 deletions
+5 -4
View File
@@ -709,11 +709,12 @@ Pulse includes a model-context layer that aggregates evidence from AI runtime su
| **Knowledge Store** | Persists user annotations and model-safe context |
| **Forecast Service** | Estimates capacity trajectories from historical samples |
### Health Scoring
### Check History
Historical Patrol checks and the legacy operational score remain available in
the collapsed supporting-context section. They are not the primary daily
monitoring answer and do not replace the canonical attention queue.
Historical Patrol checks remain available from **Activity > Finding options and
history**, under **History**. Patrol no longer shows the legacy operational
score. Check history is not the primary daily monitoring answer and does not
replace the canonical attention queue.
---
@@ -4903,8 +4903,9 @@ secondary model context after backend-refreshed finding context; those merged
references still must not become agent enrollment, install, readiness, or
fleet-command authority.
Scoped Assistant handoffs from alert or incident surfaces may also send
bounded `handoff_context`, `handoff_resources`, and Patrol assessment
`handoff_actions` through `/api/ai/chat` without a `finding_id`;
bounded `handoff_context` and `handoff_resources`, and Patrol attention
handoffs may add `handoff_actions`, through `/api/ai/chat` without a
`finding_id`;
lifecycle-adjacent surfaces may treat those references as AI/runtime
explanation context only, and must not reinterpret them as agent readiness,
install status, command reachability, enrollment health, fleet execution
@@ -3792,16 +3792,15 @@ query...`, and `Reading storage...` before streamed tool arguments are
presentation must stay compact: source, status, one primary subject, and an
optional safe route link. It must not render Patrol-authored remediation
steps, evidence chips, command summaries, recommendations, or suggested-prompt
chips as the answer. Patrol assessment handoffs must use the same
`patrol-assessment` target identity for live opens and restored sessions
rather than inheriting retired dashboard context. While such a handoff is
attached, the Assistant empty
chips as the answer. Restored Patrol assessment sessions must use the
`patrol-assessment` target identity rather than inheriting retired dashboard
context; Patrol no longer opens live assessment handoffs. While such a
session is attached, the Assistant empty
message state must also remain source-named and must not fall back to generic
cluster/system starter prompts that compete with the attached briefing.
Structured Patrol run and Patrol mode save-failure handoffs may render bounded,
redacted diagnostic lines in the drawer when they are opened directly from
Patrol runtime/control surfaces, but the attached headline must remain
source-owned (`Patrol run attached`, `Patrol mode ... attached`)
Structured Patrol run handoffs may render bounded, redacted diagnostic lines
in the drawer when they are opened directly from Patrol run history, but the
attached headline must remain source-owned (`Patrol run attached`)
and the drawer must still exclude raw provider payloads, commands, and
Patrol-authored remediation steps.
Platform tabs in that same `frontend-modern/src/App.tsx` shell may remain
@@ -4509,13 +4508,14 @@ resolve canonical/source IDs and unique aliases before collection, reject
Assistant explains the current Patrol record rather than only the original
investigation narrative. The saved-session handoff envelope must also
preserve first-class Patrol source identity when product callers provide
safe metadata. Patrol assessment handoffs remain `patrol_assessment`
whole-surface review sessions even when their bounded action references
name individual findings; the session list must not infer a
`patrol_finding` identity from those action references once metadata is
present. Patrol mode save-failure handoffs remain
`patrol_configuration_failure` sessions for compatibility and may expose only the safe
runtime-failure boolean needed for browser presentation. Run-specific
safe metadata. Saved Patrol assessment sessions (the browser no longer
creates them) remain `patrol_assessment` whole-surface review sessions even
when their bounded action references name individual findings; the session
list must not infer a `patrol_finding` identity from those action
references once metadata is present. Saved Patrol mode save-failure
sessions remain `patrol_configuration_failure` sessions for compatibility
and may expose only the safe runtime-failure boolean needed for browser
presentation. Run-specific
fields stay reserved for `patrol_run` handoffs, while hidden model context,
command payloads, preflight output, and action results remain
backend-owned. The finding briefing may surface the primary
@@ -5505,17 +5505,11 @@ factor by the ratio of errored runs to relevant runs in the scoring
window. Above 50% of recent runs erroring is a `-30` impact and is
described as "Most recent Patrol runs encountered errors"; above 25%
is `-20`; otherwise the original `-10` light-tier description applies.
This prevents the score chip from showing grade A while the same
assessment surface warns the operator that coverage is incomplete or
recent runs failed, which previously happened whenever one successful
manual run sat among many failed startup runs.
Downstream Assistant handoffs must treat that coverage factor as a
secondary caveat when Patrol also carries active findings, pending
approvals, or governed action references. The coverage-gap explanation
and scoped-activity prompt are primary Assistant framing only for
coverage-only assessments; active Patrol findings keep the prompt,
briefing action label, and safety note focused on finding priority,
affected resources, evidence, and the governed next step.
This keeps the grade from reading A when most recent runs errored, which
previously happened whenever one successful manual run sat among many failed
startup runs. A light error share still deducts only 10 points, and three
trailing successful full runs drop the penalty entirely. The Patrol page no longer renders the score; it reads the overall
health only to qualify the findings empty state.
Absence-based auto-resolve paths in `internal/ai/patrol_ai.go` are
all gated on the category whitelist exposed by
@@ -2014,8 +2014,10 @@ payload shape change when the portal presents compact client rows.
descriptions. That safe projection owns Patrol handoff identity as data,
not browser inference: `kind` may identify `patrol_assessment`,
`patrol_configuration_failure`, `patrol_run`, `patrol_finding`, or generic
`scoped_context`; assessment and configuration-failure sessions must not be
collapsed into one finding because a safe action reference contains a
`scoped_context`; the frontend no longer produces `patrol_assessment` or
`patrol_configuration_failure`, so those kinds come from sessions saved by
earlier builds, and assessment and configuration-failure sessions must not
be collapsed into one finding because a safe action reference contains a
finding ID. `patrol_configuration_failure` may carry only the
runtime-failure boolean needed for drawer/session presentation, and
run-specific fields remain reserved for `patrol_run`. Chat requests that
@@ -3197,7 +3199,7 @@ a new API state machine, queue contract, or verification-accounting field.
what Patrol may do under the selected control level, and what to run or
review next, not activation-loop proof, queue internals, or verification
accounting
and the Patrol control dialog/cross-surface Assistant handoff copy, so API
and the Patrol control dialog plus restored Patrol mode Assistant session copy, so API
compatibility identifiers such as `patrol_configuration_failure` may remain
stable while user-facing and model-facing labels describe setting, saving,
and reviewing Patrol control rather than a generic configuration/apply flow
@@ -3307,36 +3309,8 @@ a new API state machine, queue contract, or verification-accounting field.
and the Patrol page refresh lifecycle in `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, so slow or stalled secondary reads from that shared dashboard-load bundle may continue resolving in the background while the operator-facing Patrol refresh control remains generation-aware, timeout-bounded, and reusable once Patrol findings and status are already visible
and the Patrol header support drawer in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, so API-owned Patrol status and trigger facts can feed a secondary Schedule & model surface without turning provider model, schedule, trigger tuning, or background-only runtime-policy pauses into the primary Patrol control decision
and any frontend presentation of the AI summary's `policy_posture` counts, which must render that canonical snapshot through one shared component instead of page-local posture loops; no frontend surface renders those counts today, and the resource drawer stays on per-resource policy lines
and the dedicated `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` owner, so any Patrol recent-change, learned-correlation, or policy-coverage summary text is derived from the canonical AI payload in that one place instead of as hook-local count and pluralization logic; Patrol renders no such summary today
and the dedicated `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` owner, so if Patrol recent-change, learned-correlation, or policy-coverage summary text returns, it is derived from the canonical AI payload in that one place instead of as hook-local count and pluralization logic; Patrol renders no such summary today and the model carries no such derivation
and the Watch-only forward-path handoffs owned by `frontend-modern/src/features/patrol/patrolControlPresentation.ts` together with the Actions inbox empty-state read of `GET /api/ai/patrol/autonomy`, so finding-level and inbox-level mode guidance consumes the canonical Patrol autonomy read/save contract through the Patrol state hook (`handleAutonomyChange`) instead of introducing a second mode mutation path or a page-local autonomy dialect
and that same Patrol investigation-context owner, so the current Patrol
assessment summary may open Assistant with bounded model-only assessment,
verification, latest-run, supporting-context, active-finding, and
resource reference context while leaving prioritization and next-step
reasoning to the configured LLM; active
finding entries may carry live pending Patrol approval posture only as safe
structured handoff actions with approval ID/status/risk/target/request/expiry
metadata, action plan identity/policy/expiry, dry-run posture, and command
counts instead of pasting page-local UI text or raw command payloads into chat,
the drawer target must stay `patrol-assessment` rather than a retired
dashboard target,
and may derive model-only approval/action posture from that same safe
metadata, while visible drawer copy stays compact and does not expose
Patrol-authored recommendations, prompt chips, or action labels as the
answer,
and that same Patrol investigation-context owner, so coverage signals from
the canonical AI summary remain secondary caveats when the same assessment
carries active findings, pending approvals, or governed action references;
Assistant context, briefing action labels, and safety notes must
carry finding priority, affected resources, evidence, and governed approval
posture as context instead of recasting the whole handoff as a coverage gap
and that same Patrol investigation-context owner, so coverage-incomplete
assessments with no active infrastructure findings are serialized as a
verification-gap handoff: model-only context and visible briefing copy must explain what
scoped activity did and did not prove, keep latest-run and supporting-context
facts model-only, and leave next-step selection to the configured LLM
handoff-only metadata, and avoid introducing backend fields beyond the
existing Patrol status plus run-history contracts
and that same Patrol investigation-context owner, so visible Assistant
drawer handoffs may include live pending-approval metadata only as safe
operator context: approval ID, status, risk, requested/expiry timestamps,
@@ -3354,7 +3328,7 @@ a new API state machine, queue contract, or verification-accounting field.
and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops
and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior
and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` cards' optional `buildResourceHref` input, so a surface that needs resource-filter links passes them into the shared card instead of rebuilding infrastructure URLs inline; the cards carry no link default since the cross-resource drilldown links were retired, and render resource labels as plain text
and the Patrol runtime-remediation destination shared with the AI settings endpoint, so summary actions, run-history runtime-failure actions, and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination in Patrol as provider configuration instead of generic `AI Settings` copy
and the Patrol runtime-remediation destination shared with the AI settings endpoint, so run-history runtime-failure actions and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination in Patrol as provider configuration instead of generic `AI Settings` copy
and the Patrol route-shell destination itself, so the thin page shell at `frontend-modern/src/pages/AIIntelligence.tsx` may continue to bridge the shared AI-runtime payload boundary while exposing `/patrol` as the canonical product route and keeping retired `/ai` browser entry points unregistered
and the Patrol route-shell accessibility boundary, so brand icons in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx` stay decorative when the same heading already exposes visible Patrol text, preventing duplicate accessible names such as `Pulse Patrol Patrol`
and the Patrol mode selector boundary, so the default header and the
@@ -3482,8 +3456,9 @@ a new API state machine, queue contract, or verification-accounting field.
15. Keep Patrol status transport semantics explicit in that same AI handler layer: the Patrol status endpoint must carry machine-readable runtime availability such as blocked, running, disabled, active, or unavailable rather than asking frontend consumers to infer operator state from stale summaries or run history.
16. Keep legacy Patrol quickstart transport semantics retired from the public v6 GA contract: ordinary AI settings and Patrol status payloads must not expose quickstart credit/status fields, and any stale hosted-model blocked copy that survives from compatibility state must normalize back to provider/local-model setup rather than presenting credit badges or acquisition prompts.
17. Keep Patrol intelligence summary transport semantics single-voiced: the canonical overall-health payload and Patrol run-history payload together must support one primary assessment plus one explicit verification explanation, and frontend consumers must not need to derive a second compact assessment or verification verdict row from the same payloads beneath the primary assessment strip.
That same transport split now supports the visible Patrol assessment and
action metadata without adding another API field: the frontend summary
That same transport split supports Patrol assessment and action metadata
for any consumer without adding another API field (the Patrol page renders
no assessment strip today): the frontend summary
contract derives compact state from existing overall-health, run-history,
active finding, runtime, and pending-approval facts, so the API remains the
source of facts while the configured LLM owns next-step reasoning. Those
@@ -3495,7 +3470,7 @@ a new API state machine, queue contract, or verification-accounting field.
compact operator summary, but it must not imply a hero, card, duplicate
verdict layout, or expanded assessment panel. Normal Patrol page consumers
should keep explanatory assessment, verification, and activity context in
the owning Findings and Runs surfaces rather than re-expanding the primary
the owning Findings and Runs surfaces rather than reintroducing an expanded
assessment strip.
18. Keep Pulse Mobile relay credential minting and permission ownership on backend ownership: `internal/api/router_routes_auth_security.go`, `internal/api/security_tokens.go`, `internal/api/auth.go`, `internal/api/relay_mobile_capability.go`, `internal/api/router_routes_ai_relay.go`, and `frontend-modern/src/api/security.ts` may expose the canonical mobile runtime token creator and governed route gates, but browser callers must only consume that route and must not define the mobile runtime scope, compatibility gate list, route inventory, or token-purpose metadata locally.
19. Keep hosted tenant browser-session precedence on the shared auth boundary: `internal/api/auth.go`, `internal/api/contract_test.go`, and hosted tenant callers must treat a valid `pulse_session` as authoritative before any API-only token fallback or no-local-auth anonymous fallback, so cloud handoff can continue into protected hosted routes without flattening the operator back to `anonymous` or forcing a browser session through bearer-token-only mode after the tenant has minted API tokens.
@@ -4323,7 +4298,7 @@ the authoritative analysis outcome.
entitlements.
11. Treat Patrol summary supporting metrics as readouts, not reinterpretations: when frontend consumers derive cards such as active findings, criticals, warnings, or fixes from the canonical payloads, those cards must stay numeric and must not synthesize new assessment labels like `Issues detected` or verification labels like `Partial verification` beneath the primary summary contract
12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not replace the activity strip with a second assessment verdict derived from runtime state alone
13. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside the primary summary card
13. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside any primary summary card
14. Treat Patrol findings counts as a singular supporting surface as well: when the summary shell already exposes count cards for active findings, warnings, criticals, and fixes, the primary assessment card must not repeat those same payload-derived counts as secondary badges
15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the primary header and verification shell
16. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice
@@ -7168,12 +7143,13 @@ boundary: unless the operator explicitly asks for discovery execution, live
verification, or a read attempt, the runtime withholds tools and returns the
attached context or an explicit missing-fact answer rather than treating the
question as permission to call discovery/read tools. Frontend-visible Patrol
assessment briefings must not render recommendation fields as separate title,
reason, route-action facts, or prompt chips; the configured model owns those
decisions from the structured handoff metadata and bounded chat context.
Frontend handoff builders may send these fields for owned alert, incident,
Patrol assessment, Patrol finding, or Patrol run-history context, but the
backend must not persist them as user-authored message text, and the frontend
briefings, including restored assessment sessions, must not render
recommendation fields as separate title, reason, route-action facts, or prompt
chips; the configured model owns those decisions from the structured handoff
metadata and bounded chat context. Frontend handoff builders may send these
fields for owned alert, incident, Patrol attention, or Patrol finding context
(Patrol run-history handoffs send only safe run metadata: run ID, type,
status, and a runtime-failure flag; the backend rebuilds their context), but the backend must not persist them as user-authored message text, and the frontend
must not turn their evidence or proposed remedies into user-authored message
text. Ordinary attachment is context only. A labelled Explain action may
submit the shared user-selected explanation request over the existing chat
@@ -8692,7 +8668,7 @@ handoff helpers may consume current pending
approval list payloads only as safe metadata for that visible briefing and any
structured `handoff_actions`: approval ID, status, risk, request/expiry
timestamps, target label, requester identity, action ID, approval policy,
plan expiry, and dry-run summary are allowed. Assessment-level visible
plan expiry, and dry-run summary are allowed. Finding-level visible
briefings may reuse that same safe metadata for factual action labels and safety notes,
while approval command text remains inside the governed approval/remediation
surface.
@@ -1915,10 +1915,10 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`,
Session-load and new-conversation transitions must be success-bound: if the
underlying session operation fails, the shared drawer store must not clear or
replace the current scoped handoff context.
Live Patrol assessment drawer opens must use that same
Restored Patrol assessment sessions use that same
`patrol-assessment`/`pulse-patrol-assessment` target identity rather than a
retired dashboard target, so first-open and restored-session chrome remain
source-named.
retired dashboard target, so restored-session chrome remains source-named;
Patrol no longer opens live assessment handoffs.
The shared `frontend-modern/src/components/shared/AIModelPicker.tsx`
primitive must keep model route presentation delegated to the AI runtime
label helpers. Pulse-owned local Assistant routes such as
@@ -2140,17 +2140,17 @@ render it only when more than one vCenter is connected: the page passes
cell drop together, and the inline detail row's column span follows. A table
must not hide a column by filtered rows, because a search that narrows to one
vCenter would otherwise make the column vanish.
Patrol's primary assessment strip is descriptive only; it must not render a
Patrol-authored recommended next step, suggested prompt chips, or a secondary
action band inside the assessment shell. If the same assessment opens
Assistant, the Patrol-to-Assistant handoff must carry only bounded evidence,
Patrol renders no primary assessment strip today. If one returns it is
descriptive only; it must not render a Patrol-authored recommended next step,
suggested prompt chips, or a secondary action band inside the assessment shell.
Patrol-to-Assistant handoffs must carry only bounded evidence,
resource references, and factual governed approval/action metadata as model-only
context. Feature-owned Assistant handoffs may provide source context and safe
metadata, but the shared drawer boundary must not turn those handoffs into
frontend-authored prompts, tool routes, or remediation plans; the configured
model owns tool choice and diagnostic reasoning after the request reaches the
AI runtime.
The compact Patrol assessment strip may include factual recent activity mix and
Such a compact Patrol assessment strip may include factual recent activity mix and
trigger-mode labels when those values are derived from the Patrol run-history
and status payloads. Those labels are summary context inside the same strip,
not a replacement status card, CTA band, or page-local nested card.
@@ -3163,15 +3163,16 @@ Agent`), with the plain-language source phrase available through accessible
that model instead of visually falling back to the default selection.
Successful provider-model saves that return a not-ready Patrol
readiness snapshot must use that same inline surface with `needs attention`
wording, while Assistant receives a saved configuration issue rather than a
failed-save handoff. When governed fixes are locked, the same Patrol state
wording, and any Assistant handoff from that state describes a saved
configuration issue rather than a failed save. When governed fixes are locked, the same Patrol state
owner must clear stale full-mode unlock state before persisting the
monitor-only autonomy payload, so disabled paid controls cannot leak stale
permission into a save. If that inline state opens Assistant, the Patrol
feature must hand off
a source-named, model-only briefing and close the popover so the shared
Assistant drawer is not visually hidden behind feature chrome. When a Patrol
assessment handoff is attached, the shared Assistant drawer empty state must
Assistant drawer is not visually hidden behind feature chrome. When a
restored Patrol assessment session is attached, the shared Assistant drawer
empty state must
stay aligned with that source-named briefing and must not render generic
cluster/system starter prompts below the Patrol-owned context. The Patrol
feature shell must also consume the Patrol-owned findings source for its
@@ -3726,30 +3727,25 @@ production table, router and styles; it does not qualify full-app scrolling.
`Rollback not specified` lines into the model-only Patrol finding
prompt context so the operator-visible gap is surfaced to Assistant
rather than hidden, and shared chat primitives stay free of that
placeholder logic. Patrol assessment-level handoffs must use
that same feature helper to attach bounded model-only assessment,
verification, latest-run, supporting-context evidence, active-finding, and
resource reference context while forcing request-local approval-required
mode. Patrol run-history handoffs must also use that feature helper rather
placeholder logic. Patrol run-history handoffs must also use that feature helper rather
than a row-local Assistant prompt, so the shared drawer receives only a
generic visible briefing plus bounded model-only run context, scoped
resource references, runtime failure summary/detail, and
`autonomousMode:false` while the Patrol feature remains the source of run
copy and retry/configuration guidance. Active-finding entries in that
assessment handoff may add live pending
approval posture only as safe structured metadata: approval ID, pending
status, risk, target, requested/expiry timestamps, action plan identity,
requester identity, approval policy, plan expiry, dry-run posture, and
command count. Those entries may be passed through shared chat transport as
`handoff_actions` for
model-only refresh, but the shared drawer stays a generic shell rather than
a Patrol summary prompt builder. The Patrol helper may turn those same safe
references into visible action labels and safety notes for assessment and
finding-level handoffs, but it must not produce Patrol-authored suggested
prompt chips, recommendation titles, recommendation reasons, or route-owned
next-step actions. Assessment-level Patrol prompts, action labels, and
safety notes must describe active findings, pending approvals, governed
action references, and coverage caveats as evidence for the configured
generic visible briefing, safe run identity metadata, and
`autonomousMode:false`; the backend rebuilds the bounded model-only run
context, scoped resource references, and runtime failure detail from the
run ID, while the Patrol feature remains the source of run copy and
retry/configuration guidance. Finding handoff action references
may add live pending approval posture only as safe structured metadata:
approval ID, pending status, risk, target, requested/expiry timestamps,
action plan identity, requester identity, approval policy, plan expiry, and
dry-run posture. Those references may be passed through shared chat
transport as `handoff_actions` for model-only refresh, but the shared drawer
stays a generic shell rather than a Patrol summary prompt builder. The
Patrol helper may turn those same safe references into visible action
labels and safety notes for finding-level handoffs, but it must not produce
Patrol-authored suggested prompt chips, recommendation titles,
recommendation reasons, or route-owned next-step actions. Patrol finding
prompts, action labels, and safety notes must describe findings, pending
approvals, and governed action references as evidence for the configured
model, not as a frontend-authored decision tree.
Finding-level drawer opens may also pass one bounded
model-only finding context, one target resource reference, and one
@@ -4494,23 +4490,21 @@ than carrying an inline copy, so hover and inline never drift apart on
what Patrol actually owns: watching infrastructure, detecting issues,
recording findings, and escalating into governed investigation/action
only when the selected Patrol mode allows it.
The same `PatrolIntelligenceHeader.tsx` shell also renders a compact
trust-at-a-glance summary directly under the page title (a
render-only consumer of `state.patrolStatus()?.trust`), gated on at
least one non-zero trust signal so fresh installs render no header
strip. The detailed breakdown stays in
`PatrolIntelligenceWorkspace.tsx` for the canonical view; the header
line is the entry-point summary so operators see active, regressed,
and verified-fix counts before scrolling into the workspace tabs.
The same `PatrolIntelligenceHeader.tsx` shell renders no
trust-at-a-glance summary under the page title (Patrol's contract forbids a
second header trust line), and no page renders a trust counter row:
`state.patrolStatus()?.trust` only feeds the state hook's derived counts
(historical regressions and prior Patrol work).
The recency line beside the header actions also renders coverage
alongside time when the canonical `getPatrolRecencyPresentation` helper
returns `resourcesCheckedLabel` from the latest completed run. Render code
must gate on `<Show when={recency().resourcesCheckedLabel}>` (truthy) so
zero-coverage runs do not surface a misleading coverage phrase, failed or
scoped runs use neutral checked wording, and only successful full patrols read
as verified. The primary Patrol assessment shell must pass the same run-history
facts into `getPatrolAssessmentPresentation` so assessment coverage caveats do
not contradict the header's verified full-run coverage state.
as verified. Any Patrol assessment shell must pass the same run-history facts
into `getPatrolAssessmentPresentation` so assessment coverage caveats do not
contradict the header's verified full-run coverage state; the page renders no
assessment shell today.
The same header row may surface `Trigger status` when
`getPatrolTriggerStatusSummary` returns a runtime-relevant value from the
Patrol status payload. That text is page-owned operational metadata inside the
@@ -4907,11 +4901,12 @@ or not fully verified.
The same empty-state helper must consume Patrol trust-history evidence so a
historical regression reads as history review context, not as a current issue
and not as a healthy all-clear.
The same hierarchy also applies inside the Patrol summary shell: once the
primary assessment strip states Patrol's current risk and verification basis,
The Patrol page renders no summary shell today. If one returns, the same
hierarchy applies inside it: once its primary assessment strip states Patrol's
current risk and verification basis,
supporting metrics under that strip must stay metric-oriented and must not
repeat assessment or verification labels as a second compact verdict row.
The collapsed Patrol assessment strip itself must remain a compact readout
Its collapsed assessment strip must remain a compact readout
rather than a headline-plus-paragraph block; explanatory assessment and
recommendation copy belongs in the owning Findings, Runs, or Assistant chat
surfaces rather than a normal-path summary details expansion.
@@ -6779,12 +6774,9 @@ feature shell, reads its active workspace view from the route through
`patrolWorkspaceRouting.ts` rather than holding it in component state, `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`
owns the runtime state machine, `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
owns the pure Patrol-to-Assistant handoff and operator briefing derivation,
including the rule that active findings, pending
approvals, and governed action references outrank secondary coverage caveats
when building the Assistant prompt, action label, and safety note,
`frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` owns canonical AI
summary normalization at the shared store boundary, and the Patrol-owned
header/banner/summary/workspace section files under
header, banner, attention workbench, workspace, and panel section files under
`frontend-modern/src/features/patrol/` own the heavy render surfaces. Shared
shell governance should reinforce that pattern instead of letting feature render
surfaces re-accumulate API and timer orchestration inline.
@@ -6796,7 +6788,10 @@ That same rule also applies to compact Patrol summary fragments inside the
feature surface: count-only strips or metric cards must not emit `No issues
found` or other reassuring copy when the owning overall-health summary is
degraded or not fully verified.
That same summary shell should also surface verification scope from the
Patrol renders no summary shell today; the header recency line shows the
latest check or activity time, how many resources that check covered, and the
next scheduled check.
If a summary shell returns, it should also surface verification scope from the
owning run-history contract. Operators should be able to see, inside the same
summary surface, whether Patrol recently completed a full verification pass or
whether recent activity was limited to scoped/erroring patrol runs.
@@ -7210,9 +7205,9 @@ wording.
Resource-change readers follow the same split: `getResourceChangePresentation`
and `formatResourceChangeHeadline` in `utils/resourceChangePresentation.ts`
own the `Alert moved` label, its neutral-blue tone and the summary headline
for an `alert_resolved` change with `alert_resolution` metadata, and the
Patrol assessment handoff consumes that headline instead of composing its own
kind prefix.
for an `alert_resolved` change with `alert_resolution` metadata, and
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
consumes that headline instead of composing its own kind prefix.
`frontend-modern/src/features/alerts/useAlertHistoryState.ts` re-exposes the
`getResource` resolver it is already given, and
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx` reads it
@@ -1085,10 +1085,11 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
present. The panel must not invent, smooth, or client-side infer a forecast;
a missing forecast simply means no urgency line renders.
The Patrol page must not render a standalone Trust strip above the
Findings/Runs tab bar or a parallel header trust line. The primary
Patrol assessment readout is the default owner for current operator
state, and may include high-signal trust facts such as
`state.patrolStatus()?.trust.regressed_at_least_once` (the
Findings/Runs tab bar or a parallel header trust line. Patrol renders no
primary assessment readout today; the header control row and the
attention workbench carry current operator state. If a readout returns,
it owns current operator state and may include high-signal trust facts
such as `state.patrolStatus()?.trust.regressed_at_least_once` (the
FindingsTrustSummary block on the patrol-status response) only as
compact status text. New trust categories must extend the
`FindingsTrustSummary` contract first rather than inventing
@@ -1153,45 +1154,18 @@ fix`, or `Explain` based on current finding state), while secondary
than dropping it; runtime-failure findings produced by
`internal/ai/patrol_runtime_failure.go` are the first source to depend
on that path reaching the Patrol page surface.
The primary Patrol assessment summary may open Assistant as a whole-surface
review handoff, but that handoff must also flow through
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`.
The summary action may pass the current assessment title, health score,
verification recency, latest run, secondary investigation context, bounded
recent-change and learned-correlation evidence, active-finding summaries,
structured resource references, structured approval/action references, and
model-only context. Assessment-level handoffs must
identify the drawer target as
`patrol-assessment` with `targetId=pulse-patrol-assessment`, matching saved
session restore semantics rather than the retired dashboard target.
Active-finding
summaries may include live pending approval posture only as safe metadata:
approval ID, pending status, risk, target, requested/expiry timestamps,
action plan identity, approval policy, plan expiry, dry-run posture, and
command count. They must force request-local approval-required mode, keep raw
command and approval payloads out of prompt and drawer copy, surface visible
drawer action posture from the same safe references, make the initial prompt
lead with approval/action review when governed references are attached, and
frame Assistant as explanation, prioritization, and model-owned next-step reasoning
rather than a generic reactive chat
box. That whole-surface assessment handoff must send safe
`handoff_metadata.kind=patrol_assessment` so saved Assistant sessions restore
as current-assessment context instead of becoming generic scoped context or
an accidental single-finding session because one bounded action reference
names a finding. Saved assessment and finding sessions must not expose or
restore Patrol-authored next-step titles, recommendation detail, action
labels, or app-route hrefs through `handoff_summary`; legacy stored
recommendation fields are ignored rather than converted into hidden context.
Assessment Assistant drawer briefings must stay compact and source-named
instead of presenting a recommended step title, reason, route action, or
suggested prompt chips as operator-facing answers, so
provider-setting/runtime-visibility failures remain plain context for the
configured model rather than Pulse-authored recovery guidance.
When the current Patrol assessment is coverage-incomplete with no active
infrastructure finding, the same handoff model may describe incomplete
coverage as evidence, but it must let the configured model decide whether
more verification, operator action, or governed tool use is needed. Execution
or retry remains operator-controlled.
Patrol no longer opens a whole-surface assessment handoff: the summary card
that offered it and its builder in
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` are
gone. Assistant sessions saved from that handoff still restore through the
shared drawer as `handoff_metadata.kind=patrol_assessment` with the
`patrol-assessment` target and `targetId=pulse-patrol-assessment`, never as
generic scoped context or as a single-finding session because one bounded
action reference names a finding. Saved assessment and finding sessions must
not expose or restore Patrol-authored next-step titles, recommendation
detail, action labels, or app-route hrefs through `handoff_summary`; legacy
stored recommendation fields are ignored rather than converted into hidden
context.
Patrol run-history entries may also open Assistant for a selected run, but
that handoff must flow through the same Patrol-owned investigation-context
model rather than a row-local prompt. The browser-visible prompt and drawer
@@ -1209,8 +1183,11 @@ fix`, or `Explain` based on current finding state), while secondary
frame Assistant as explanation and next-step review rather than execution or
automatic retry authority.
9. Keep the normal Patrol status summary plain and operator-first rather
than a hero-style or decorative card surface. The default collapsed state is
a compact readout, not a headline block: show the Patrol status label,
than a hero-style or decorative card surface. The default page renders no
status summary card or assessment strip today: the header control row
carries Patrol state, mode, trigger status, and recency. If a collapsed
status readout returns, it is a compact readout, not a headline block: show
the Patrol status label,
current operator state, concise high-signal trust posture such as
regressions when present, and score. Do not combine reassuring grade labels such as
`Health A` with issue-state copy such as `Issues detected` in the collapsed
@@ -1399,12 +1376,11 @@ condition went, and that it is not a recovery) sits directly under the summary
rather than only inside Evidence and history (`getAttentionHandoverClose` in
`patrolHomePresentation.ts`). The timeline names that cause `Ownership
transferred`, never `Recovery evidence`.
The assessment handoff tells the same close the same way: a recent
`alert_resolved` change carrying `alert_resolution` metadata enters the
handoff context and briefing evidence as the alert engine's summary through
the shared `formatResourceChangeHeadline`, never as `Alert resolved: ...`
(`patrolInvestigationContextModel.ts`, pinned by
`patrolInvestigationContextModel.test.ts`).
Any Patrol Assistant handoff that attaches recent changes must tell the same
close the same way: an `alert_resolved` change carrying `alert_resolution`
metadata reads as the alert engine's summary through the shared
`formatResourceChangeHeadline`, never as `Alert resolved: ...`. No Patrol
handoff attaches recent changes today.
The active Patrol queue now uses compact severity-accented rows for
prioritisation and a single focused review panel for the selected issue. The
@@ -1543,27 +1519,29 @@ once the shared catalog supplies it, not fall back visually to the default
model or an unrelated OpenRouter entry because the popover mounted after the
catalog request completed.
The Patrol page now also treats Patrol runtime availability as a first-class
render contract: the header chip, primary summary card, and status bar must
all route through the shared `frontend-modern/src/utils/patrolRuntimePresentation.ts`
render contract: any Patrol surface that presents runtime availability must
route through the shared `frontend-modern/src/utils/patrolRuntimePresentation.ts`
helper plus the backend `runtime_state` payload instead of inferring operator
state from the last healthy summary snapshot or run history alone.
The primary summary strip is descriptive only: it surfaces the current Patrol
assessment label and compact counts, while action choices remain in the
Findings/Runs workspace, header controls, and the LLM-driven Assistant chat.
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` may
package the current Patrol assessment, verification posture, latest run,
secondary investigation context, bounded recent-change and learned-correlation
evidence, bounded active-finding summaries, deduped resource references, and
safe structured approval/action references as model-only context, but visible
drawer copy must not promote Pulse-authored next-step metadata, action chips, or
suggested prompts as the answer. Command payloads stay out of drawer chrome.
Patrol renders no primary summary strip today. If one returns it stays
descriptive only: it surfaces the current Patrol assessment label and compact
counts, while action choices remain in the workspace, header controls, and the
LLM-driven Assistant chat.
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`
packages the Patrol finding, attention-item, and run handoffs, carrying only
bounded model-only context, resource references, and safe structured
approval/action references, and visible drawer copy must not promote
Pulse-authored next-step metadata, action chips, or suggested prompts as the
answer. Command payloads stay out of drawer chrome.
Run-history rows now follow that same Assistant handoff model. A selected
`frontend-modern/src/components/patrol/RunHistoryEntry.tsx` row may open
Assistant through
`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` with a
`[Patrol Run Context]` block, scoped resource references, runtime failure
summary/detail when present, bounded outcome and coverage facts, and a visible
`Patrol run attached` drawer briefing. The row must not create a second
visible `Patrol run attached` drawer briefing (bounded outcome and coverage
facts plus a redacted runtime failure summary when present) and safe run
identity metadata; the backend (`internal/ai/patrol_assistant_handoff.go`)
builds the model-only `[Patrol Run Context]` block and scoped resource
references from the run ID. The row must not create a second
prompt-only Assistant shortcut or imply that Assistant can retry Patrol,
change provider configuration, or execute remediation from run-history context.
That active-runtime label must stay operational rather than verdict-like: the
@@ -1583,21 +1561,22 @@ viewport, avoid duplicating the inline Patrol control policy, expose
provider/model, schedule, trigger, and user-level model checks directly, and surface
the backend's concrete license/validation reason when a settings change is
rejected instead of replacing it with a generic `Failed to save advanced
settings` toast. That inline failure may
handoff to Assistant only as model-only explanation context: raw command,
script, credential, and provider-detail payloads stay redacted, Assistant opens
with `autonomousMode:false`, and the Patrol control panel closes so the operator
is not left behind an overlapping popover. Patrol-control save-failure handoffs
must keep the compatible `handoff_metadata.kind=patrol_configuration_failure` plus only
the runtime-failure boolean needed for drawer/session presentation, so the
saved session restores as a Patrol control issue without carrying raw
provider, credential, command, or retry payloads into the browser.
settings` toast. That inline failure no longer
opens Assistant. If it does again, the handoff is model-only explanation
context: raw command, script, credential, and provider-detail payloads stay
redacted, Assistant opens with `autonomousMode:false`, and the Patrol control
panel closes so the operator is not left behind an overlapping popover.
Patrol-control save-failure sessions saved by earlier builds keep the
compatible `handoff_metadata.kind=patrol_configuration_failure` plus only the
runtime-failure boolean needed for drawer/session presentation, so they restore
as a Patrol control issue without carrying raw provider, credential, command,
or retry payloads into the browser.
Successful provider-model saves that return
`patrol_readiness.status=not_ready` are still Patrol control issues, not silent
successes: the Patrol popover must keep the saved provider/model visible,
render a `Patrol control needs attention` inline state with the returned
readiness cause and summary, and hand off to Assistant as a model-only Patrol
control issue rather than as a save failure.
readiness cause and summary; any Assistant handoff from that state must
describe a Patrol control issue rather than a save failure.
A separate browser proof in
`tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts` keeps the
authenticated launch workbench monitor-first. When the runtime has monitored
@@ -1618,11 +1597,12 @@ control state owner must also clamp stale investigation/remediation
autonomy back to findings-only `monitor` and clear stale full-mode unlock state
before persisting Patrol control when the safe-remediation entitlement is not
effective, so an expired or downgraded plan cannot turn a recoverable control
review into a Pro-only save failure. The same inline error
surface must render any Patrol readiness
provider, model, and summary carried by the failure object, and keep the
provider-settings action immediately available before the operator chooses to
open the Assistant handoff.
review into a Pro-only save failure. Patrol renders no inline error
surface for these failures today: switching Patrol on or off while it is not
ready raises a fixed warning notification, a failed Patrol mode save raises an error
notification, and neither offers an Assistant handoff. A provider or model save
in Settings > Provider & Models reports the readiness summary, provider and
model in its own save message.
The Patrol mode selector in that header and configuration dialog must
compose the shared `frontend-modern/src/components/shared/FilterButtonGroup.tsx`
instead of rebuilding a local active-button group. The wide default Patrol
@@ -1719,12 +1699,6 @@ consumers must not pair an `Issues detected` headline with a raw coverage-only
`overall_health.prediction` sentence from a separate source; when active
findings and incomplete verification are both true, the assessment should
describe both in one canonical message.
The same precedence applies to Patrol-to-Assistant assessment handoffs. An
active finding, pending approval, or governed action reference must remain the
primary Assistant prompt and briefing posture even when the attached model
context carries a secondary coverage caveat. Coverage-incomplete language such
as explaining scoped activity or a full-run gap is the primary handoff framing
only for coverage-only assessments with no active findings.
That same assessment contract must also distinguish Patrol-owned runtime
findings from infrastructure findings. When the only active Patrol findings are
synthetic Patrol service/runtime conditions such as the `ai-service`
@@ -1747,18 +1721,18 @@ inviting generic Assistant chat.
When that recommendation has an immediate governed operator path, the same
helper must also declare the bounded action kind. The summary card may map
those action kinds only to existing Patrol controls: run a full Patrol,
review approval-scoped findings, review active findings, open Patrol provider
settings, or open the Patrol-owned Assistant assessment handoff. It must not
review approval-scoped findings, review active findings, or open Patrol
provider settings. It must not
invent page-local remediation, approval, or execution authority from
recommendation text.
That same runtime-owned assessment must expose the fix path directly. When the
primary Patrol issue is a Patrol runtime/provider problem rather than an
infrastructure finding, the summary card should offer a direct `Open Patrol
If a summary card returns, that same runtime-owned assessment must expose the
fix path directly. When the primary Patrol issue is a Patrol runtime/provider
problem rather than an infrastructure finding, the summary card should offer a direct `Open Patrol
provider settings` action instead of making the operator dig through the
findings list to discover where to correct provider configuration.
That same runtime-versus-infrastructure distinction should route through the
shared finding-presentation helper instead of being re-inferred separately by
the summary card and the findings list. The active finding row should surface
any 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
@@ -1798,9 +1772,9 @@ rather than repeating the product prefix or legacy `Insufficient API credits`
wording once the surrounding UI already makes the Patrol context explicit.
That same finding presentation contract should own the primary remediation path
for Patrol-owned runtime findings as well. Expanded runtime-finding rows should
lead with the same direct `Open Provider & Models` action that the top
assessment uses, and that action must use the canonical Pulse Intelligence
route `/settings/pulse-intelligence/provider` rather than the legacy
lead with the shared `getPatrolProviderSettingsAction()` action (`Check Patrol
model`), and that action must use the canonical Pulse Intelligence
route `/settings/pulse-intelligence/patrol` rather than the legacy
`/settings/system-ai` compatibility alias. Runtime/setup findings in the
default current-issues flow must stay primary-action-only: they should not add
a visible `Open in Assistant` secondary button or a generic `Manage` menu
@@ -1846,14 +1820,14 @@ work or fix-verification checks without claiming a new full verification pass.
That same recency contract also applies to the header metadata row. The top
header must not revert to a generic `Last:` timestamp when the rest of Patrol
is explicitly distinguishing activity from full verification recency.
That summary surface must also avoid reintroducing a second compact assessment
or verification layer beneath the primary card. Supporting metric strips
If Patrol renders a summary surface again, it must also avoid reintroducing a
second compact assessment or verification layer beneath the primary card. Supporting metric strips
belong to counts and outcomes such as active findings, critical findings,
warnings, and fixes; they must not repeat Patrol assessment labels or
verification labels in a second row that competes with the primary governed
assessment and verification copy above.
The same supporting-chip rule applies to timing: the primary summary card may
show health and active-finding support, but it should not add another recency
The same supporting-chip rule applies to timing: such a primary summary card
may show health and active-finding support, but it should not add another recency
pill once header metadata, verification, and findings footer already carry the
governed activity/verification timestamps.
That same Patrol-owned run-history surface must keep platform-backed system
@@ -1865,8 +1839,9 @@ That run-history distinction must not reopen a parallel raw `truenas`
resource-type contract in Patrol findings, scoped-run filters, or alert-backed
resource state. Those payloads stay canonical `agent` plus platform context,
while the separate `TrueNAS` count remains a run-history coverage detail.
That summary-card/metrics-strip split also applies to findings counts. The
primary assessment card may keep health as supporting context, but active
If Patrol renders a summary card again, the same summary-card/metrics-strip
split also applies to findings counts: the primary assessment card may keep
health as supporting context, but active
findings, warning counts, and critical counts belong to the supporting metric
strip rather than being repeated as duplicate badges inside the primary card.
That same summary surface must also explain what Patrol actually checked
@@ -1924,8 +1899,8 @@ header settings surface should expose alert-triggered and anomaly-triggered
scoped patrols as separate controls, with the legacy aggregate event-trigger
toggle treated as compatibility-only transport rather than the primary product
model.
The findings empty state must also stay subordinate to the Patrol header and
assessment shell rather than mirroring their timing metadata. In the primary
The findings empty state must also stay subordinate to the Patrol header
rather than mirroring its timing metadata. In the primary
Patrol page, `frontend-modern/src/components/AI/FindingsPanel.tsx` should
explain the absence of active findings without repeating `Last activity`,
`Next run`, or interval schedule details that already belong to the header and
@@ -2345,7 +2320,8 @@ from the canonical helper rather than maintaining a parallel copy, so
hover and inline never tell different stories about what Patrol does.
The Patrol page header must not add a second trust-at-a-glance line under
the page title. Current active/regressed/finding posture belongs to the
primary assessment readout and the finding rows, so the header remains
workspace below the tabs and its finding rows (Patrol renders no primary
assessment readout today), so the header remains
focused on product title, recency, and route controls instead of repeating
the same trust counters above the workspace tabs.
The same page-header recency line must also surface the coverage
@@ -2021,7 +2021,7 @@ Assistant handoff context may hydrate those normalized action-audit facts for
review, but that read is still model-only context: it must remain org-scoped,
must not expose raw command text or raw execution output, and must not grant
approval or execution authority.
Scoped Assistant `handoff_actions` from Patrol assessment handoffs may carry
Scoped Assistant `handoff_actions` from Patrol finding and attention handoffs may carry
only safe approval/action metadata for model-only refresh, including approval
IDs, action IDs, policy, expiry, dry-run posture, and proposed-fix labels; they
must not expose raw command or execution payloads or become an approval bypass.
@@ -1961,7 +1961,7 @@ recovery scope, or a storage/recovery-owned secret source.
details attached to that briefing are review context only and must not
become backup freshness evidence, restore eligibility, storage health truth,
or recovery execution authority.
Alert, incident, and Patrol assessment Assistant handoffs that send bounded
Alert, incident, and Patrol attention Assistant handoffs that send bounded
model-only `handoff_context`, `handoff_resources`, or `handoff_actions`
through `/api/ai/chat` without a `finding_id` stay on that same adjacent
AI/runtime boundary. Storage and recovery surfaces may consume the
@@ -4270,8 +4270,7 @@ the `ResourceChangeSummary` badge reads `Alert moved` in the neutral blue of
the alert history's "moved to agent" badge (`Alert closed` for a reason code
the build does not know), and
`formatResourceChangeHeadline` uses the summary as the whole headline instead
of prefixing `Alert resolved:`; Patrol's assessment context reuses that
headline. A node's own drawer does not list the move yet: node and guest
of prefixing `Alert resolved:`. A node's own drawer does not list the move yet: node and guest
alert changes are still recorded under their legacy alert resource IDs
(`MonitorAdapter.RecordChange` maps only Docker IDs), and Proxmox platform
rows open the drawer in the `table-row` presentation, which fetches no
@@ -0,0 +1,38 @@
{
"version": 1,
"base_sha": "d67bef7519cd1206f4f5b61f9aa4a25bb5348cda",
"verified_at": "2026-10-07T14:18:14Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts"
],
"content_sha256": {
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts": "537a76c9a3beda65a52b92c4a2298111cf747511f154a2d9fc983b4601a37336"
},
"routes": [
"/patrol",
"/patrol/activity"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Tree = PR 2674 head d67bef7519 plus this change; production vite build embedded in a go build binary, private DEMO_MODE mock stack on :7686 (demo/demo, PULSE_TELEMETRY=false, fresh data dir), driven by Playwright",
"Runtime-neutral proof: production builds of the parent tree and of this tree are byte-identical (all 187 dist files equal by shasum), so both removed builders and their helpers were already tree-shaken; check-bundle-size.mjs passes; the bundle holds neither 'Discuss Patrol assessment' nor 'Patrol mode save failure attached', and still holds the restore-path title 'Patrol mode save failure'",
"Assistant drawer after the Inbox decision's 'Explain with Assistant' shows the Pulse Patrol attention briefing ('critical \u00b7 open', the selected decision's resource, 'Approval required before any action.') from buildPatrolAttentionAssistantHandoff; the demo instance refuses the reply itself ('Demo mode enabled'), which is expected read-only behaviour",
"Assistant drawer after the selected run's 'Discuss' shows the 'Patrol run attached' briefing with the run id, timing, outcomes and the operator-controlled safety note from buildPatrolRunAssistantHandoff",
"No page errors at either viewport"
],
"interactions": [
"Loaded /patrol, opened the first row of 'Patrol attention items' and clicked 'Explain with Assistant', then closed the drawer",
"Loaded /patrol, switched to Activity, expanded 'Finding options and history', clicked History, opened the newest run (check details), returned to History and clicked the selected run's 'Discuss'",
"Repeated every step at 1440x900 and 390x844 (phone context with touch)"
]
}
+5 -4
View File
@@ -709,11 +709,12 @@ Pulse includes a model-context layer that aggregates evidence from AI runtime su
| **Knowledge Store** | Persists user annotations and model-safe context |
| **Forecast Service** | Estimates capacity trajectories from historical samples |
### Health Scoring
### Check History
Historical Patrol checks and the legacy operational score remain available in
the collapsed supporting-context section. They are not the primary daily
monitoring answer and do not replace the canonical attention queue.
Historical Patrol checks remain available from **Activity > Finding options and
history**, under **History**. Patrol no longer shows the legacy operational
score. Check history is not the primary daily monitoring answer and does not
replace the canonical attention queue.
---
@@ -12,7 +12,6 @@ import {
buildPatrolAssistantFindingHandoffInputFromUnifiedFinding,
buildPatrolAssistantProposedFixBriefingInput,
buildPatrolAssistantProposedFixBriefingInputFromApproval,
buildPatrolConfigurationFailureHandoff,
buildPatrolInvestigationRecordPresentation,
buildPatrolRunAssistantHandoff,
} from '../patrolInvestigationContextModel';
@@ -464,72 +463,6 @@ describe('buildPatrolAssistantFindingHandoffInputFromUnifiedFinding (branch cove
});
});
describe('buildPatrolConfigurationFailureHandoff (branch coverage)', () => {
it('falls back to the default message and bare subject when message and code are empty', () => {
const handoff = buildPatrolConfigurationFailureHandoff({ message: '' });
expect(handoff.context.briefing?.subject).toBe('Patrol mode could not be saved.');
expect(handoff.context.briefing?.title).toBe('Patrol mode save failure attached');
expect(handoff.context.briefing?.statusLabel).toBeUndefined();
expect(handoff.context.briefing?.detailLines).toEqual([]);
});
it('derives the cause from blockedCause when readiness is absent', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
blockedCause: 'provider_unavailable',
});
expect(handoff.context.briefing?.statusLabel).toBe('provider_unavailable');
expect(handoff.context.briefing?.detailLines).toEqual(['Cause: Provider Unavailable']);
expect(handoff.context.context?.readinessCause).toBe('provider_unavailable');
});
it('renders the full requested-settings line for an enabled full-mode configuration', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
autonomyLevel: 'approval',
fullModeUnlocked: true,
investigationBudget: 5,
investigationTimeoutSec: 30,
});
expect(handoff.context.briefing?.detailLines).toContain(
'Requested settings: mode approval, automatic critical fixes enabled, budget 5, timeout 30s',
);
});
it('surfaces provider and model when readiness carries only those fields', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
readiness: { provider: 'ollama', model: 'm1' },
});
expect(handoff.context.briefing?.detailLines).toEqual(['Provider: ollama', 'Model: m1']);
expect(handoff.context.context?.provider).toBe('ollama');
expect(handoff.context.context?.model).toBe('m1');
});
it('withholds sensitive keys and command-bearing values while showing safe details', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
details: {
db_password: 'secret123',
normal_field: 'visible value',
cmd_value: 'operator ran sudo rm',
},
});
expect(handoff.context.briefing?.evidence).toEqual([
'Db Password: sensitive or command detail withheld',
'Normal Field: visible value',
'Cmd Value: sensitive or command detail withheld',
]);
expect(JSON.stringify(handoff)).not.toContain('secret123');
expect(JSON.stringify(handoff)).not.toContain('sudo rm');
expect(JSON.stringify(handoff)).toContain('visible value');
});
});
describe('buildPatrolAssistantFindingBriefing (branch coverage)', () => {
it('returns undefined when there are no finding facts, record, approval, or fix', () => {
expect(buildPatrolAssistantFindingBriefing({ title: 'T', subject: 'S' })).toBeUndefined();
@@ -1,16 +1,12 @@
import { describe, expect, it } from 'vitest';
import type { RemediationPlan } from '@/api/ai';
import type { InvestigationRecord } from '@/api/ai';
import type { ResourceCorrelation } from '@/types/aiIntelligence';
import type { ResourceChange } from '@/types/resource';
import type { PatrolRunRecord } from '@/api/patrol';
import {
buildPatrolAssessmentAssistantHandoff,
buildPatrolAssistantFindingBriefing,
buildPatrolAssistantFindingHandoff,
buildPatrolAssistantFindingHandoffActions,
buildPatrolConfigurationFailureHandoff,
buildPatrolInvestigationRecordPresentation,
buildPatrolRemediationPlanAssistantBriefing,
buildPatrolRemediationPlanAssistantModelContext,
@@ -164,520 +160,6 @@ describe('buildPatrolRunAssistantHandoff (residual branches)', () => {
});
});
describe('buildPatrolConfigurationFailureHandoff (residual branches)', () => {
it('emits the Cause line using the identifier-formatted cause', () => {
// cause truthy arm of the Cause detail-line ternary.
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
readiness: { cause: 'model_unsupported_tools' },
});
expect(handoff.context.briefing?.detailLines).toContain('Cause: Model Unsupported Tools');
expect(handoff.context.context?.readinessCause).toBe('model_unsupported_tools');
});
it('omits the Cause line entirely when no cause is available', () => {
const handoff = buildPatrolConfigurationFailureHandoff({ message: 'm' });
expect(handoff.context.briefing?.detailLines).toEqual([]);
expect(handoff.context.briefing?.statusLabel).toBeUndefined();
});
it('withholds a detail whose key or value normalizes to empty', () => {
// formatSafeConfigurationFailureDetail early-return when key or value is empty.
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
details: {
' ': 'visible',
empty_value: ' ',
good: 'kept',
},
});
expect(handoff.context.briefing?.evidence).toEqual(['Good: kept']);
});
it('passes a digits-only detail key through the identifier formatter unchanged', () => {
// formatIdentifierLabel('123') returns '123' - there is no [._-] to expand
// and no letter to title-case - so the label is the key verbatim WITHOUT
// the || fallback being taken. The genuine empty-formatter fallback arm is
// covered separately by the separator-only key case below.
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
details: { '123': 'value-here' },
});
expect(handoff.context.briefing?.evidence).toEqual(['123: value-here']);
});
});
describe('buildPatrolAssessmentAssistantHandoff (residual branches)', () => {
it('falls back to the default assessment title in both briefing and model context', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { description: 'only a description' },
activeFindings: [],
});
expect(handoff.context.briefing?.subject).toBe('Pulse Patrol assessment');
expect(handoff.context.handoffContext).toContain('Assessment: Pulse Patrol assessment');
});
it('drops findings that carry no id, title, resourceId, or record id, and tolerates a null list', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
activeFindings: [
{ severity: 'critical' },
{ title: 'kept-by-title' },
{ resourceId: 'kept-by-resource' },
{ investigationRecord: { id: 'rec-kept' } as unknown as InvestigationRecord },
],
});
// activeFindingCount reflects the raw input length, but the severity-only
// finding (no id/title/resourceId/record id) is filtered out of the model
// context, so only three Finding lines render.
expect(handoff.context.context?.activeFindingCount).toBe(4);
expect(handoff.context.handoffContext).toContain('Finding 1: kept-by-title');
expect(handoff.context.handoffContext).toContain('Finding 3: Patrol finding');
expect(handoff.context.handoffContext).not.toContain('Finding 4:');
});
it('counts active findings as zero and omits actions when activeFindings is null', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({ activeFindings: null });
expect(handoff.context.context?.activeFindingCount).toBe(0);
expect(handoff.context.context?.pendingApprovalCount).toBe(0);
expect(handoff.context.handoffActions).toBeUndefined();
});
it('reports omitted findings and an omitted singular recent change in the model context', () => {
const findings = Array.from({ length: 6 }, (_, i) => ({
id: `f-${i + 1}`,
title: `Finding ${i + 1}`,
}));
const changes = Array.from({ length: 4 }, (_, i) => ({
id: `c-${i + 1}`,
observedAt: '2026-01-01T00:00:00Z',
resourceId: `vm-${i + 1}`,
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'high',
reason: `reason ${i + 1}`,
})) as unknown as ResourceChange[];
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: { recentChanges: changes },
activeFindings: findings,
});
// 6 findings, cap 5 -> 1 omitted (singular wording).
expect(handoff.context.handoffContext).toContain(
'1 additional Patrol finding omitted from this bounded handoff summary.',
);
// 4 recent changes, cap 3 -> 1 omitted (singular wording).
expect(handoff.context.handoffContext).toContain(
'1 additional recent change omitted from this bounded handoff summary.',
);
});
it('renders a finding context line with no resource, no id, and no status parts', () => {
// Bare finding: resource resolves to nothing, finding.id empty, no status
// fields -> each ternary takes its undefined arm.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: [{ title: 'Bare finding' }],
});
expect(handoff.context.handoffContext).toContain('Bare finding on affected resource');
// No "finding <id>" segment, no status segment -> the line is exactly the
// title-on-resource form with no trailing parts.
const ctx = handoff.context.handoffContext!;
const line = ctx.split('\n').find((l) => l.startsWith('Finding 1:'));
expect(line).toBe('Finding 1: Bare finding on affected resource');
});
it('surfaces record error, regression singular, and a divergent record approval id', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: [
{
id: 'f-1',
title: 'High CPU',
regressionCount: 1,
pendingApproval: { id: 'pa-1', status: 'approved', riskLevel: 'high' },
investigationRecord: {
id: 'rec-1',
finding_id: 'f-1',
subject: { resource_id: 'vm-1' },
trigger: { detected_at: '2026-01-01T00:00:00Z' },
status: 'completed',
error: 'investigation aborted',
approval_id: 'rec-approval',
} as unknown as InvestigationRecord,
},
],
});
const line = handoff.context
.handoffContext!.split('\n')
.find((l) => l.startsWith('Finding 1:'))!;
expect(line).toContain('regressed 1 time');
expect(line).toContain('investigation error investigation aborted');
expect(line).toContain('approval rec-approval');
// Non-pending approval posture is rendered as inline approval parts:
// "approved approval" (status) and "high risk" (risk label).
expect(line).toContain('approved approval');
expect(line).toContain('high risk');
});
it('renders metric fallback labels, singularized custom and non-plural labels, and the no-signal summary', () => {
// primaryLabel absent -> default 'Active findings' label; fixedLabel
// 'Categories' with value 1 -> singular 'category' (ies rule).
const primaryAndCategories = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
metricState: { primaryValue: 1, fixedValue: 1, fixedLabel: 'Categories' },
activeFindings: [],
});
expect(primaryAndCategories.context.briefing?.statusLabel).toContain('1 active finding');
expect(primaryAndCategories.context.briefing?.statusLabel).toContain('1 category');
// secondaryLabel absent (and distinct from the explicit primary label) ->
// default 'Warnings' label.
const secondaryFallback = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
metricState: { primaryLabel: 'Active findings', primaryValue: 1, secondaryValue: 1 },
activeFindings: [],
});
expect(secondaryFallback.context.briefing?.statusLabel).toContain('1 active finding');
expect(secondaryFallback.context.briefing?.statusLabel).toContain('1 warning');
// Non-plural label ('Fixed') with value 1 -> stays 'fixed'.
const fixedOne = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
metricState: { fixedValue: 1 },
activeFindings: [],
});
expect(fixedOne.context.briefing?.statusLabel).toContain('1 fixed');
// No metric values and no findings -> default attention summary.
const empty = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: [],
});
expect(empty.context.briefing?.statusLabel).toContain('No active Patrol findings');
});
it('always emits a Health line, defaulting the chip label to "Health" when no signal is present', () => {
// formatAssessmentHealth can never be empty because the chip label itself
// defaults to 'Health'; with no grade or score the line is just that label.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain('\nHealth: Health');
});
it('includes verification sub-facts only when their timestamps are present', () => {
const withFacts = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
verification: {
title: 'Checked',
description: 'desc',
lastFullRunAt: '2026-01-01T00:00:00Z',
activityMixLabel: 'mix',
},
activeFindings: [],
});
expect(withFacts.context.handoffContext).toContain('last full run 2026-01-01T00:00:00Z');
expect(withFacts.context.handoffContext).toContain('recent activity mix mix');
const withoutFacts = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
verification: { title: 'Checked', description: 'desc' },
activeFindings: [],
});
expect(withoutFacts.context.handoffContext).not.toContain('last full run');
expect(withoutFacts.context.handoffContext).not.toContain('recent activity mix');
});
it('falls back to the Last Patrol label and returns undefined recency with no signal', () => {
const withTimestampOnly = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
recency: { timestamp: '2026-01-01T00:00:00Z' },
activeFindings: [],
});
expect(withTimestampOnly.context.handoffContext).toContain(
'Last Patrol: Last Patrol 2026-01-01T00:00:00Z',
);
const withNothing = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
recency: null,
activeFindings: [],
});
expect(withNothing.context.handoffContext).not.toContain('\nLast Patrol:');
});
it('builds sparse correlation endpoints and their context/evidence lines', () => {
// Correlation with only a source_id (no names/types, no target id) ->
// endpoint name/type collapse to undefined; the empty target id means the
// target resource is never added.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
correlations: [
{
source_id: 'src-1',
event_pattern: 'p',
occurrences: 1,
avg_delay: 1,
} as unknown as ResourceCorrelation,
],
},
activeFindings: [],
});
// Correlation context line renders the source id.
expect(handoff.context.handoffContext).toContain('Correlation 1: src-1');
// Only the source resource is added, with no name or type (empty target id
// is dropped by addAssessmentHandoffResource).
expect(handoff.context.handoffResources).toEqual([{ id: 'src-1' }]);
});
it('renders correlation context sub-facts (pattern, last seen, description) and full evidence join', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
correlations: [
{
source_id: 'src-1',
source_name: 'Source',
source_type: 'vm',
target_id: 'tgt-1',
target_name: 'Target',
target_type: 'host',
event_pattern: 'a -> b',
occurrences: 1,
avg_delay: 1,
confidence: 0.9,
last_seen: '2026-01-01T00:00:00Z',
description: 'correlated activity',
},
],
},
activeFindings: [],
});
const line = handoff.context
.handoffContext!.split('\n')
.find((l) => l.startsWith('Correlation 1:'))!;
// Both endpoints resolve -> the " to " join is used.
expect(line).toContain('Source');
expect(line).toContain(' to ');
expect(line).toContain('Target');
expect(line).toContain('last seen 2026-01-01T00:00:00Z');
expect(line).toContain('description correlated activity');
const evidence = handoff.context.briefing?.evidence ?? [];
expect(evidence.some((e) => e.includes('Source') && e.includes(' to '))).toBe(true);
expect(evidence.some((e) => e.includes('pattern'))).toBe(true);
});
it('renders sparse recent-change evidence and context sub-facts', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-1',
observedAt: '2026-01-01T00:00:00Z',
occurredAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'config_update',
sourceType: 'user_action',
sourceAdapter: 'proxmox_adapter',
confidence: 'medium',
actor: 'operator-one',
reason: 'manual edit',
},
],
},
activeFindings: [],
});
const line = handoff.context
.handoffContext!.split('\n')
.find((l) => l.startsWith('Recent Change 1:'))!;
expect(line).toContain('change c-1');
expect(line).toContain('resource vm-1');
expect(line).toContain('observed 2026-01-01T00:00:00Z');
expect(line).toContain('occurred 2026-01-01T00:00:00Z');
expect(line).toContain('source user action');
expect(line).toContain('adapter Proxmox Adapter');
expect(line).toContain('medium confidence');
expect(line).toContain('actor operator-one');
});
it('falls back to the change id then the bare resource token when no reason is present', () => {
const byId = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-id',
observedAt: '2026-01-01T00:00:00Z',
resourceId: '',
kind: 'activity',
sourceType: 'platform_event',
confidence: 'low',
},
],
},
activeFindings: [],
});
expect(byId.context.handoffContext).toContain('Recent Change 1: Activity: c-id');
const bare = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: '',
observedAt: '',
resourceId: '',
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'low',
},
],
},
activeFindings: [],
});
expect(bare.context.handoffContext).toContain('Recent Change 1: Metric anomaly: resource');
});
it('renders a state-transition summary for differing states and keeps command-bearing wording', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-trans',
observedAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'state_transition',
from: 'offline',
to: 'online',
sourceType: 'pulse_diff',
confidence: 'high',
},
{
id: 'c-runbook',
observedAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'runbook_executed',
sourceType: 'agent_action',
confidence: 'high',
reason: 'ran the playbook',
},
],
},
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain(
'Recent Change 1: State transition: offline to online',
);
expect(handoff.context.handoffContext).toContain(
'Recent Change 2: Runbook executed: execution event recorded',
);
});
it('formats the two-label related-resource list and a no-omission three-label list', () => {
const two = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-2',
observedAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'high',
reason: 'r',
relatedResources: ['alpha', 'beta'],
},
],
},
activeFindings: [],
});
expect(two.context.handoffContext).toContain('related resources alpha and beta');
const three = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-3',
observedAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'high',
reason: 'r',
relatedResources: ['alpha', 'beta', 'gamma'],
},
],
},
activeFindings: [],
});
expect(three.context.handoffContext).toContain('related resources alpha, beta, gamma');
});
it('caps assessment handoff resources at eight across many distinct resources', () => {
const findings = Array.from({ length: 5 }, (_, i) => ({
id: `f-${i + 1}`,
title: `F${i + 1}`,
resourceId: `finding-res-${i + 1}`,
}));
const changes = Array.from({ length: 3 }, (_, i) => ({
id: `c-${i + 1}`,
observedAt: '2026-01-01T00:00:00Z',
resourceId: `change-res-${i + 1}`,
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'high',
reason: 'r',
relatedResources: [`rel-${i + 1}-a`, `rel-${i + 1}-b`],
})) as unknown as ResourceChange[];
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: { recentChanges: changes },
activeFindings: findings,
});
// MAX_ASSESSMENT_RESOURCES = 8; the ninth distinct resource is dropped.
expect(handoff.context.handoffResources).toHaveLength(8);
});
it('caps assessment handoff actions at four when more action-bearing findings exist', () => {
const findings = Array.from({ length: 6 }, (_, i) => ({
id: `f-${i + 1}`,
title: `F${i + 1}`,
proposedFix: { description: `Restart ${i + 1}` },
}));
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: findings,
});
expect(handoff.context.handoffActions).toHaveLength(4);
});
});
describe('buildPatrolAssistantFindingHandoff (residual branches)', () => {
it('collapses findingId to undefined in both top-level and nested context when no id exists', () => {
const handoff = buildPatrolAssistantFindingHandoff({ title: 'T', subject: 'S' });
@@ -967,40 +449,6 @@ describe('buildPatrolAssistantFindingBriefing (residual branches)', () => {
});
});
// ---------------------------------------------------------------------------
// Residual v8 branch arms not reached by the cases above. Each test below
// targets a specific uncovered conditional arm (the `||`/`??` fallback, the
// falsy ternary alternate, or a singular/plural boundary) and asserts a real
// observable value. Arms that are provably unreachable from any public entry
// point are documented in GLM_REPORT_fe-patrolctx.md rather than faked here.
// ---------------------------------------------------------------------------
describe('buildPatrolConfigurationFailureHandoff (identifier fallback arms)', () => {
it('uses the raw cause verbatim when the identifier formatter collapses it to empty', () => {
// cause '...' is all separator chars: formatIdentifierLabel('...') returns
// '' (falsy) -> `formatIdentifierLabel(cause) || cause` falls back to the
// verbatim cause string.
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
readiness: { cause: '...' },
});
expect(handoff.context.briefing?.detailLines).toContain('Cause: ...');
expect(handoff.context.context?.readinessCause).toBe('...');
});
it('uses the raw detail key verbatim when the identifier formatter yields nothing', () => {
// formatSafeConfigurationFailureDetail: a key of all separators makes
// formatIdentifierLabel(normalizedKey) return '' -> `|| normalizedKey`.
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'm',
details: { '...': 'plain-value' },
});
expect(handoff.context.briefing?.evidence).toEqual(['...: plain-value']);
});
});
describe('buildPatrolAssistantFindingHandoffActions (findingId source fallback)', () => {
it('derives the action findingId from the investigation record when the finding has no id', () => {
// finding.id absent -> normalizeText(finding.id) is '' (falsy) -> the
@@ -1063,130 +511,6 @@ describe('buildPatrolRemediationPlanAssistantBriefing (non-array steps)', () =>
});
});
describe('buildPatrolAssessmentAssistantHandoff (correlation filter + sparse endpoints)', () => {
it('keeps a correlation that carries only an event_pattern', () => {
// The correlation filter `||` chain falls through source_id, source_name,
// target_id and target_name (all empty) and is retained by event_pattern.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
correlations: [
{
event_pattern: 'cpu-spike',
occurrences: 1,
avg_delay: 1,
} as unknown as ResourceCorrelation,
],
},
activeFindings: [],
});
// Both endpoints resolve to undefined (no source/target ids or names), so
// the context line is kept solely by its pattern/summary facts.
expect(handoff.context.handoffContext).toContain('Correlation 1:');
expect(handoff.context.handoffContext).toContain('pattern');
});
it('renders a target-only correlation and omits the pattern fact when event_pattern is absent', () => {
// source endpoint undefined, target endpoint present -> `source && target`
// is falsy -> alternate `source || target` evaluates target. With no
// event_pattern the pattern fact takes its `: undefined` arm.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
correlations: [
{
target_id: 'tgt-1',
target_name: 'Target',
target_type: 'host',
occurrences: 1,
avg_delay: 1,
} as unknown as ResourceCorrelation,
],
},
activeFindings: [],
});
const line = handoff.context
.handoffContext!.split('\n')
.find((l) => l.startsWith('Correlation 1:'))!;
expect(line).toContain('Target');
expect(line).not.toContain('pattern');
});
it('falls back to the "Items" metric label when the provided label is whitespace-only', () => {
// primaryLabel ' ' is a truthy string so the caller's `|| 'Active
// findings'` keeps it, but normalizeText trims it to '' inside
// formatAssessmentMetricCount -> `normalizeText(label) || 'Items'`. With a
// singular value the 'Items' label is singularized to 'item'.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
metricState: { primaryLabel: ' ', primaryValue: 1 },
activeFindings: [],
});
expect(handoff.context.briefing?.statusLabel).toContain('1 item');
});
it('renders a single related-resource label without an omitted-count suffix', () => {
// formatAssessmentRelatedResourceList: labels.length === 1 with omittedCount
// 0 -> the bare-label arm (the omittedCount > 0 arm is unreachable here
// because a single visible label always implies zero omitted).
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
supportingEvidence: {
recentChanges: [
{
id: 'c-1',
observedAt: '2026-01-01T00:00:00Z',
resourceId: 'vm-1',
kind: 'metric_anomaly',
sourceType: 'heuristic',
confidence: 'high',
reason: 'r',
relatedResources: ['solo'],
},
],
},
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain('related resources solo');
expect(handoff.context.handoffContext).not.toContain('and 0 more');
});
it('pluralizes the regression count in the assessment finding context line', () => {
// regressionCount > 1 -> the 's' arm of `regressionCount === 1 ? '' : 's'`.
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: [{ id: 'f-1', title: 'High CPU', regressionCount: 3 }],
});
const line = handoff.context
.handoffContext!.split('\n')
.find((l) => l.startsWith('Finding 1:'))!;
expect(line).toContain('regressed 3 times');
});
it('reports plural omitted findings when more than one is truncated', () => {
// 7 findings, cap 5 -> 2 omitted -> the 's' arm of the findings-omitted
// ternary.
const findings = Array.from({ length: 7 }, (_, i) => ({
id: `f-${i + 1}`,
title: `F${i + 1}`,
}));
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'T' },
activeFindings: findings,
});
expect(handoff.context.handoffContext).toContain(
'2 additional Patrol findings omitted from this bounded handoff summary.',
);
});
});
describe('buildPatrolAssistantFindingHandoff (regression plural arm)', () => {
it('pluralizes the regression count in the finding model context', () => {
const handoff = buildPatrolAssistantFindingHandoff({
@@ -1,17 +1,15 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import type { RemediationPlan } from '@/api/ai';
import type { InvestigationRecord, RemediationPlan } from '@/api/ai';
import type { PatrolRunRecord } from '@/api/patrol';
import {
buildPatrolAssessmentAssistantHandoff,
buildPatrolAssistantApprovalBriefingInput,
buildPatrolAssistantFindingBriefing,
buildPatrolAssistantFindingHandoff,
buildPatrolAssistantFindingHandoffActions,
buildPatrolAssistantProposedFixBriefingInput,
buildPatrolConfigurationFailureHandoff,
buildPatrolRunAssistantHandoff,
buildPatrolInvestigationRecordPresentation,
buildPatrolRemediationPlanAssistantBriefing,
@@ -20,565 +18,6 @@ import {
} from '../patrolInvestigationContextModel';
describe('patrolInvestigationContextModel', () => {
it('keeps same-state recent changes out of Patrol Assistant no-op wording', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Patrol runtime issue',
description: 'Coverage is incomplete.',
},
investigationContext: {
recentChangeCount: 1,
correlationCount: 0,
governedResourceCount: 0,
hasContext: true,
summaryText: '1 recent change',
},
supportingEvidence: {
recentChanges: [
{
id: 'change-1',
observedAt: '2026-05-06T12:08:00Z',
resourceId: 'app-container-1',
kind: 'state_transition',
from: 'online',
to: 'online',
sourceType: 'pulse_diff',
sourceAdapter: 'docker_adapter',
confidence: 'high',
reason: 'resource state changed',
metadata: {
changedFields: ['docker.command'],
},
},
],
},
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain('Docker command changed while online');
expect(handoff.context.handoffContext).not.toContain('online to online');
});
it("tells a moved alert close in the alert engine's words, not as a recovery", () => {
const summary =
'Alert moved to pve1 (Host Agent). This is not a recovery: check the agent for the current reading.';
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'Issues detected' },
supportingEvidence: {
recentChanges: [
{
id: 'change-moved',
observedAt: '2026-10-06T15:00:00Z',
resourceId: 'node:pve1',
kind: 'alert_resolved',
sourceType: 'heuristic',
confidence: 'high',
reason: summary,
metadata: { alert_resolution: 'moved_to_agent', alert_type: 'memory' },
},
],
},
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain(summary);
expect(handoff.context.handoffContext).not.toContain('Alert resolved');
expect(handoff.context.briefing?.evidence?.join('\n')).toContain(summary);
});
it('includes bounded related-resource context in assessment handoff evidence', () => {
const longRelatedResource =
'storage-pool-with-a-very-long-description-that-keeps-going-beyond-the-handoff-limit-for-operators';
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Issues detected',
},
supportingEvidence: {
recentChanges: [
{
id: 'change-related',
observedAt: '2026-05-06T12:08:00Z',
resourceId: 'vm-100',
kind: 'metric_anomaly',
sourceType: 'heuristic',
sourceAdapter: 'proxmox_adapter',
confidence: 'high',
reason: 'CPU pressure increased after storage activity',
relatedResources: [
'backup-job',
'cache-node',
longRelatedResource,
'db-primary',
'db-replica',
],
},
],
},
activeFindings: [],
});
const relatedEvidence = handoff.context.briefing?.evidence?.find((line) =>
line.includes('related resources'),
);
expect(relatedEvidence).toContain('related resources backup-job');
expect(relatedEvidence).toContain('and 1 more');
expect(relatedEvidence).not.toContain(longRelatedResource);
expect(relatedEvidence).not.toContain('db-replica');
expect(handoff.context.handoffContext).toContain('related resources backup-job');
expect(handoff.context.handoffContext).toContain('and 1 more');
expect(handoff.context.handoffContext).not.toContain(longRelatedResource);
expect(handoff.context.handoffContext).not.toContain('db-replica');
});
it('builds a model-only Assistant handoff for the current Patrol assessment', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Issues detected',
description:
'Patrol surfaced one active critical finding and recent coverage is incomplete.',
eyebrow: 'Current assessment',
},
overallHealth: { grade: 'B', score: 84 },
scoreChipLabel: 'Health',
metricState: {
primaryLabel: 'Infrastructure findings',
primaryValue: 1,
secondaryLabel: 'Runtime issues',
secondaryValue: 1,
fixedLabel: 'Fixed',
fixedValue: 2,
},
verification: {
title: 'Recently checked',
description: 'Latest full run completed with findings.',
lastFullRunAt: '2026-05-06T12:00:00Z',
activityMixLabel: '1 full check · 2 targeted checks',
},
recency: {
label: 'Last patrol',
timestamp: '2026-05-06T12:10:00Z',
},
latestRun: {
kindLabel: 'Patrol check',
status: { label: 'issues found' },
timestamp: '2026-05-06T12:10:00Z',
coverageSummary: '12 resources checked',
findingsSnapshotAvailable: true,
},
investigationContext: {
recentChangeCount: 2,
correlationCount: 2,
governedResourceCount: 4,
hasContext: true,
summaryText: '2 recent changes · 2 correlations · 4 policy-covered resources',
},
supportingEvidence: {
recentChanges: [
{
id: 'change-1',
observedAt: '2026-05-06T12:08:00Z',
occurredAt: '2026-05-06T12:07:30Z',
resourceId: 'vm-100',
kind: 'metric_anomaly',
sourceType: 'heuristic',
sourceAdapter: 'proxmox_adapter',
confidence: 'high',
actor: 'Pulse Patrol',
relatedResources: ['backup-job'],
reason: 'CPU spike after backup job',
},
{
id: 'change-2',
observedAt: '2026-05-06T12:07:00Z',
resourceId: 'vm-100',
kind: 'command_executed',
sourceType: 'agent_action',
sourceAdapter: 'agent:ops-helper',
confidence: 'medium',
reason: 'systemctl restart workload.service',
metadata: {
command: 'systemctl restart workload.service',
},
},
],
correlations: [
{
source_id: 'backup-job',
source_name: 'Nightly backup job',
source_type: 'job',
target_id: 'vm-100',
target_name: 'web-server',
target_type: 'vm',
event_pattern: 'backup_started -> cpu_spike',
occurrences: 4,
avg_delay: 120000000000,
confidence: 0.92,
last_seen: '2026-05-06T12:08:00Z',
description: 'CPU pressure usually follows this backup job.',
},
],
},
activeFindings: [
{
id: 'finding-1',
title: 'High CPU usage',
description: 'CPU stayed above 95% during backup.',
severity: 'critical',
status: 'active',
resourceId: 'vm-100',
resourceName: 'web-server',
resourceType: 'vm',
detectedAt: '2026-05-06T12:00:00Z',
lastSeenAt: '2026-05-06T12:10:00Z',
investigationStatus: 'completed',
investigationOutcome: 'fix_queued',
loopState: 'awaiting_approval',
timesRaised: 3,
regressionCount: 1,
lastRegressionAt: '2026-05-06T12:06:00Z',
investigationRecord: {
id: 'record-1',
finding_id: 'finding-1',
subject: {
resource_id: 'vm-100',
resource_name: 'web-server',
resource_type: 'vm',
node: 'pve-1',
},
trigger: { detected_at: '2026-05-06T12:00:00Z' },
status: 'completed',
outcome: 'fix_queued',
confidence: 'high',
conclusion: 'Backup job saturated CPU.',
recommended_action: 'Approve a controlled restart after the backup completes.',
evidence: [{ kind: 'metrics', summary: 'CPU stayed above 95% for 10 minutes' }],
proposed_fix: {
id: 'fix-1',
description: 'Restart the workload service',
commands: ['systemctl restart workload.service'],
risk_level: 'medium',
destructive: false,
},
verification: ['CPU returned below 50%'],
rollback: [],
tools_used: ['ssh.exec'],
started_at: '2026-05-06T12:00:00Z',
},
},
{
id: 'finding-2',
title: 'Patrol provider warning',
severity: 'warning',
status: 'active',
resourceId: 'vm-100',
resourceName: 'web-server',
resourceType: 'vm',
},
],
});
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.autonomousMode).toBe(false);
expect(handoff.context.handoffContext).toContain('[Patrol Assessment Context]');
expect(handoff.context.handoffContext).toContain('Source: Pulse Patrol current assessment');
expect(handoff.context.handoffContext).toContain('Health: Health B 84/100');
expect(handoff.context.handoffContext).toContain(
'Supporting Context: 2 recent changes · 2 correlations · 4 policy-covered resources',
);
expect(handoff.context.handoffContext).toContain(
'Recent Change 1: Metric anomaly: CPU spike after backup job',
);
expect(handoff.context.handoffContext).toContain('related resources backup-job');
expect(handoff.context.briefing?.evidence).toEqual(
expect.arrayContaining([expect.stringContaining('related resources backup-job')]),
);
expect(handoff.context.handoffContext).toContain(
'Recent Change 2: Command executed: execution event recorded',
);
expect(handoff.context.handoffContext).toContain('Correlation 1: Nightly backup job');
expect(handoff.context.handoffContext).toContain('Finding 1: High CPU usage');
expect(handoff.context.handoffContext).toContain('1 command recorded for approval context');
expect(handoff.context.handoffResources).toEqual([
{ id: 'vm-100', name: 'web-server', type: 'vm', node: 'pve-1' },
{ id: 'backup-job', name: 'Nightly backup job', type: 'job', node: undefined },
]);
expect(handoff.context.handoffMetadata).toEqual({
kind: 'patrol_assessment',
});
expect(handoff.context.briefing).toMatchObject({
sourceLabel: 'Pulse Patrol',
title: 'Patrol assessment attached',
subject: 'Issues detected',
actionLabel: '1 governed action reference attached',
safetyNote:
'Review action posture in the governed flow · raw command payloads stay out of Assistant.',
});
expect(JSON.stringify(handoff)).not.toContain('systemctl restart workload.service');
});
it('uses operator-facing finding-record wording for legacy latest-run context', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Patrol reviewed recent activity',
},
latestRun: {
kindLabel: 'Patrol check',
status: { label: 'completed' },
timestamp: '2026-05-06T12:10:00Z',
coverageSummary: '67 resources checked',
findingsSnapshotAvailable: false,
},
activeFindings: [],
});
expect(handoff.context.handoffContext).toContain('finding record unavailable');
expect(handoff.context.handoffContext).not.toContain('findings snapshot unavailable');
});
it('frames coverage-incomplete assessment handoffs as evidence-only context', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Coverage incomplete',
description:
'Recent Patrol activity only covered targeted checks. Run Patrol to check everything.',
eyebrow: 'Patrol assessment',
},
overallHealth: {
grade: 'C',
score: 65,
prediction: 'Patrol coverage is incomplete.',
factors: [{ category: 'coverage' }],
},
scoreChipLabel: 'Assessment',
metricState: {
primaryLabel: 'Active findings',
primaryValue: 0,
secondaryLabel: 'Warnings',
secondaryValue: 0,
fixedLabel: 'Fixed',
fixedValue: 0,
},
verification: {
title: 'Recently checked',
description: 'The most recent Patrol check completed successfully.',
lastFullRunAt: '2026-05-04T21:38:51Z',
activityMixLabel: '8 full checks, 3 alert-triggered checks',
},
latestRun: {
kindLabel: 'Targeted check',
status: { label: 'issues found' },
timestamp: '2026-05-07T21:39:18Z',
coverageSummary: 'Checked 1 of 2 scoped resources',
findingsSnapshotAvailable: true,
},
investigationContext: {
recentChangeCount: 100,
correlationCount: 29,
governedResourceCount: 116,
hasContext: true,
summaryText: '100 recent changes · 29 correlations · 116 policy-covered resources',
},
activeFindings: [],
});
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.briefing).toMatchObject({
actionLabel: 'Discuss Patrol coverage',
safetyNote:
'Assistant can review the coverage evidence. Patrol runs, diagnostics, and remediation remain governed controls.',
});
expect(handoff.context.briefing?.actionHref).toBeUndefined();
expect(handoff.context.handoffContext).toContain('Assessment: Coverage incomplete');
expect(handoff.context.handoffContext).not.toContain('Recommended Next Step');
expect(handoff.context.handoffContext).toContain(
'Supporting Context: 100 recent changes · 29 correlations · 116 policy-covered resources',
);
expect(handoff.context).toMatchObject({
targetType: 'patrol-assessment',
targetId: 'pulse-patrol-assessment',
autonomousMode: false,
});
expect(handoff.context.context).not.toHaveProperty('recommendedNextStepTitle');
expect(handoff.context.context).not.toHaveProperty('recommendedNextStepActionKind');
expect(handoff.context.handoffMetadata).toEqual({
kind: 'patrol_assessment',
});
});
it('surfaces active findings as facts rather than Patrol recommendations', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Issues detected',
description:
'Patrol surfaced one active warning finding in your infrastructure. Review the active findings for more detail.',
eyebrow: 'Patrol assessment',
},
overallHealth: {
grade: 'B',
score: 85,
prediction: 'Coverage is incomplete for secondary activity.',
factors: [{ category: 'coverage' }],
},
verification: {
title: 'Recently checked',
description:
'The most recent Patrol check completed successfully and covered 58 resources.',
lastFullRunAt: '2026-05-12T21:22:35Z',
activityMixLabel: '8 full checks, 3 alert-cleared checks',
},
latestRun: {
kindLabel: 'Patrol check',
status: { label: 'issues found' },
timestamp: '2026-05-12T21:22:35Z',
coverageSummary: 'Checked 58 resources',
findingsSnapshotAvailable: true,
},
investigationContext: {
recentChangeCount: 3,
correlationCount: 70,
governedResourceCount: 55,
hasContext: true,
summaryText: '3 recent changes · 70 correlations · 55 policy-covered resources',
},
activeFindings: [
{
id: 'finding-backup',
title: 'Backup failed',
severity: 'warning',
status: 'active',
resourceId: 'backup-delly',
resourceName: 'delly',
resourceType: 'backup',
},
],
});
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.briefing).toMatchObject({
actionLabel: 'Discuss Patrol assessment',
safetyNote: 'Use diagnostic tools within current permissions. New actions remain governed.',
});
expect(handoff.context.handoffContext).not.toContain('Recommended Next Step');
});
it('carries live governed approval posture into assessment finding handoffs', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: {
title: 'Issues detected',
},
activeFindings: [
{
id: 'finding-1',
title: 'High CPU usage',
severity: 'critical',
status: 'active',
resourceId: 'vm-100',
resourceName: 'web-server',
resourceType: 'vm',
pendingApproval: {
id: 'approval-1',
status: 'pending',
riskLevel: 'high',
requestedAt: '2026-05-06T12:00:00Z',
expiresAt: '2026-05-06T12:10:00Z',
targetName: 'web-server',
actionId: 'action-1',
actionApprovalPolicy: 'admin',
actionPlanExpiresAt: '2026-05-06T12:10:00Z',
actionPlanMessage: 'Restart after the backup window clears.',
actionPreflight: 'Restart workload service',
actionDryRunSummary: 'No provider-supported dry run is available for this action.',
actionRequestedBy: 'pulse_patrol',
},
proposedFix: {
description: 'Restart the workload service',
riskLevel: 'high',
targetHost: 'web-server',
commandCount: 1,
destructive: true,
},
},
],
});
expect(handoff.context.autonomousMode).toBe(false);
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.handoffContext).toContain('Finding 1: High CPU usage');
expect(handoff.context.handoffContext).toContain('approval approval-1');
expect(handoff.context.handoffContext).toContain('live approval pending');
expect(handoff.context.handoffContext).toContain('high risk');
expect(handoff.context.handoffContext).toContain('approval target web-server');
expect(handoff.context.handoffContext).toContain('expires 2026-05-06T12:10:00Z');
expect(handoff.context.handoffContext).toContain('requested by pulse_patrol');
expect(handoff.context.handoffContext).toContain('1 command recorded for approval context');
expect(handoff.context.handoffContext).toContain('destructive action artifact');
expect(handoff.context.handoffActions).toHaveLength(1);
expect(handoff.context.handoffActions?.[0]).toMatchObject({
findingId: 'finding-1',
approvalId: 'approval-1',
approvalStatus: 'pending',
approvalRequestedAt: '2026-05-06T12:00:00Z',
approvalExpiresAt: '2026-05-06T12:10:00Z',
actionId: 'action-1',
actionRequestedBy: 'pulse_patrol',
actionApprovalPolicy: 'admin',
actionRequiresApproval: true,
actionPlanExpiresAt: '2026-05-06T12:10:00Z',
actionPlanMessage: 'Restart after the backup window clears.',
actionPreflight: 'Restart workload service',
actionDryRunSummary: 'No provider-supported dry run is available for this action.',
description: 'Restart the workload service',
riskLevel: 'high',
destructive: true,
targetHost: 'web-server',
targetResourceId: 'vm-100',
targetResourceName: 'web-server',
targetResourceType: 'vm',
});
expect(handoff.context.context).toMatchObject({
pendingApprovalCount: 1,
});
expect(handoff.context.briefing).toMatchObject({
actionLabel: '1 pending governed approval attached',
safetyNote:
'Review approvals in the governed flow · approval policy is attached · dry-run posture is attached · destructive actions remain approval-bound · raw command payloads stay out of Assistant.',
});
expect(JSON.stringify(handoff)).not.toContain('systemctl restart workload.service');
});
it('keeps a typed pending action approval-bound without a legacy approval id', () => {
const handoff = buildPatrolAssessmentAssistantHandoff({
assessment: { title: 'Issues detected' },
activeFindings: [
{
id: 'finding-typed-action',
title: 'Unhealthy workload',
severity: 'warning',
status: 'active',
resourceId: 'docker:container:web',
pendingApproval: {
id: '',
status: 'pending_approval',
riskLevel: 'governed',
requestedAt: '2026-07-10T18:00:00Z',
actionId: 'action-typed-1',
actionApprovalPolicy: 'admin',
actionRequestedBy: 'pulse_patrol',
},
},
],
});
expect(handoff.context.handoffActions?.[0]).toMatchObject({
findingId: 'finding-typed-action',
actionId: 'action-typed-1',
actionApprovalPolicy: 'admin',
actionRequiresApproval: true,
});
expect(handoff.context.handoffActions?.[0].approvalId).toBeUndefined();
});
it('builds a model-only Assistant handoff for a Patrol run runtime failure', () => {
const run: PatrolRunRecord = {
id: 'run-runtime-error',
@@ -656,72 +95,6 @@ describe('patrolInvestigationContextModel', () => {
expect(JSON.stringify(handoff)).not.toContain('No endpoints found');
});
it('builds a model-only Assistant handoff for a Patrol mode save failure', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
message: 'The selected Patrol model is a reasoning-only model family.',
code: 'patrol_readiness_not_ready',
status: 400,
details: {
cause: 'model_unsupported_tools',
provider: 'ollama',
command: 'systemctl restart pulse.service',
},
autonomyLevel: 'approval',
fullModeUnlocked: false,
investigationBudget: 10,
investigationTimeoutSec: 120,
readiness: {
status: 'not_ready',
cause: 'model_unsupported_tools',
summary: 'The selected Patrol model is a reasoning-only model family.',
provider: 'ollama',
model: 'ollama:deepseek-r1:7b',
},
runtimeState: 'active',
});
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.autonomousMode).toBe(false);
expect(handoff.context.handoffMetadata).toMatchObject({
kind: 'patrol_configuration_failure',
runtimeFailure: true,
});
expect(handoff.context.handoffContext).toContain('[Patrol Control Save Failure Context]');
expect(handoff.context.handoffContext).toContain('Server Code: patrol_readiness_not_ready');
expect(handoff.context.handoffContext).toContain('Provider: ollama');
expect(handoff.context.handoffContext).toContain('Model: ollama:deepseek-r1:7b');
expect(handoff.context.handoffContext).toContain(
'Command: sensitive or command detail withheld',
);
expect(handoff.context.briefing).toMatchObject({
sourceLabel: 'Pulse Patrol',
title: 'Patrol mode save failure attached',
actionLabel: 'Review Patrol mode save failure',
});
expect(JSON.stringify(handoff)).not.toContain('systemctl restart pulse.service');
});
it('labels saved Patrol mode readiness issues separately from save failures', () => {
const handoff = buildPatrolConfigurationFailureHandoff({
saved: true,
message: 'Patrol was saved, but the selected model cannot run Patrol tools yet.',
code: 'patrol_readiness_not_ready',
readiness: {
status: 'not_ready',
cause: 'model_unsupported_tools',
summary: 'The selected Patrol model cannot run tools.',
provider: 'ollama',
model: 'ollama:deepseek-r1:7b',
},
});
expect(handoff).not.toHaveProperty('prompt');
expect(handoff.context.briefing).toMatchObject({
title: 'Patrol mode issue attached',
actionLabel: 'Review Patrol mode issue',
});
});
it('builds operator-facing Patrol record presentation without exposing raw commands', () => {
const presentation = buildPatrolInvestigationRecordPresentation({
id: 'record-1',
@@ -978,6 +351,33 @@ describe('patrolInvestigationContextModel', () => {
).toEqual([]);
});
it('keeps a typed pending action approval-bound without a legacy approval id', () => {
const [action] = buildPatrolAssistantFindingHandoffActions({
id: 'finding-typed-action',
title: 'Unhealthy workload',
severity: 'warning',
status: 'active',
resourceId: 'docker:container:web',
pendingApproval: {
id: '',
status: 'pending_approval',
riskLevel: 'governed',
requestedAt: '2026-07-10T18:00:00Z',
actionId: 'action-typed-1',
actionApprovalPolicy: 'admin',
actionRequestedBy: 'pulse_patrol',
},
});
expect(action).toMatchObject({
findingId: 'finding-typed-action',
actionId: 'action-typed-1',
actionApprovalPolicy: 'admin',
actionRequiresApproval: true,
});
expect(action.approvalId).toBeUndefined();
});
it('builds finding-level Assistant handoff context, resources, and actions together', () => {
const handoff = buildPatrolAssistantFindingHandoff({
id: 'finding-1',
@@ -1050,6 +450,34 @@ describe('patrolInvestigationContextModel', () => {
expect(JSON.stringify(handoff)).not.toContain('systemctl restart nginx');
});
it('qualifies the finding resource with its record node and bounds long context lines', () => {
const description = 'Disk latency spiked during the backup window. '.repeat(20).trim();
const handoff = buildPatrolAssistantFindingHandoff({
id: 'finding-node',
title: 'Disk latency',
subject: 'web-server',
description,
investigationRecord: {
id: 'record-node',
finding_id: 'finding-node',
subject: {
resource_id: 'vm-100',
resource_name: 'web-server',
resource_type: 'vm',
node: 'pve1',
},
status: 'completed',
} as unknown as InvestigationRecord,
});
const lines = handoff.context.handoffContext?.split('\n') ?? [];
expect(lines).toContain('Resource: web-server (vm vm-100 node pve1)');
const descriptionLine = lines.find((line) => line.startsWith('Description: '));
expect(description.length).toBeGreaterThan(500);
expect(descriptionLine?.endsWith('...')).toBe(true);
expect(descriptionLine?.slice('Description: '.length).length).toBeLessThanOrEqual(500);
});
it('builds a drawer briefing for Assistant handoff without exposing raw commands', () => {
// Pin "now" so the relative-time formatting in the briefing
// (`last regression {relative}`) is deterministic against the
File diff suppressed because it is too large Load Diff
@@ -121,6 +121,11 @@ class AIRuntimeDocsPolicyTest(unittest.TestCase):
self.assertNotIn("Pulse Assistant should be replaced by", normalized_content)
self.assertNotIn("MCP replaces Pulse Assistant", normalized_content)
self.assertNotIn("Assistant can be removed", normalized_content)
# The Patrol summary card and its supporting-context section are gone,
# so the docs point at where check history actually lives.
self.assertIn("Activity > Finding options and history", normalized_content)
self.assertNotIn("legacy operational score remain", normalized_content)
self.assertNotIn("collapsed supporting-context section", normalized_content)
self.assertIn("- **Anthropic** (API key)", content)
self.assertIn("Anthropic OAuth is not a supported runtime authentication method", normalized_content)
self.assertIn("does not make Anthropic configured", normalized_content)
@@ -304,7 +309,7 @@ class AIRuntimeDocsPolicyTest(unittest.TestCase):
self.assertIn("## Patrol Modes", control_doc)
self.assertIn("Patrol mode sets how far Pulse can go", control_doc)
self.assertIn("**UI:** Patrol → Patrol mode", control_doc)
self.assertIn("The API keeps the autonomy_level field name for compatibility.", control_doc)
self.assertIn("The API retains `autonomy_level` for compatibility", control_doc)
self.assertNotIn("Patrol Control Level", control_doc)
self.assertNotIn("## Patrol Control Levels", control_doc)
self.assertNotIn("Patrol control is configured on the **Patrol** page.", control_doc)