Files
pulse/internal/alerts
rcourtman d26a483731 Show a moved alert as moved in Patrol, history and assistant (#2591)
Follow-up to rcourtman/Pulse#2580. A node alert handed to its Pulse
agent closes with an `AlertResolution` (`moved_to_agent`), and
notifications already say it moved. Every in-app reader still treated
that close as a recovery:

- Patrol's attention detail showed a green **Resolved** badge, and its
timeline read "Open to Resolved · Recovery evidence" directly above
"…This is not a recovery…".
- The Alerts history listed the row as **resolved**, and its incident
timeline ended "Alert resolved: Node memory at 95.0%".
- `GetRecentlyResolved` rebuilt `models.Alert` without the resolution.
So the state snapshot, websocket, assistant context, Patrol's seed
prompt and the `pulse_alerts` `resolved` action all saw a plain resolve.

**Operational trust.** New `TransitionCause` `ownership_transferred`. It
may only enter `resolved`, and only with evidence.
`markOperationalResolved` uses it whenever the alert carries a
`moved_to_agent` resolution; every other close stays on
`recovery_evidence`. Alert-quality telemetry keys "resolved while
snoozed" on `recovery_evidence`, so a handover during a snooze no longer
counts there. No notification code reads the cause: links key on the
transition ID and state.

**State and AI.** `models.Alert.Resolution` holds the reason, successor
and the engine's `Summary()`. `cloneAlert` copies it, and
`GetRecentlyResolved` fills it.
- Patrol's seed writes `closed 3m ago. Alert moved to pve1 (Host Agent).
This is not a recovery: …`.
- The assistant's context writes `closed … ago) <summary>`.
- The resolved-alerts tool returns `resolution` and
`successor_resource_id`.

**Timelines.** The `alert_resolved` resource change carries the summary
as its reason and the code as `alert_resolution` metadata. Incident
memory titles the close with it, both when projecting from the canonical
timeline and on its own write path.

**UI.**
- The history row reads **moved to agent**, even if it was acknowledged.
The full account is in the badge title and is printed under the message
on the phone card.
- Patrol's detail shows a **Moved** badge (info tone), with the handover
sentence directly under the summary rather than only inside Evidence and
history. Its timeline says "Ownership transferred".

**Landing order.** rcourtman/Pulse#2586 is the only producer of a
handover, and it has no auto-merge. It should land after this PR (or
merge main once this is in). Any handover recorded before this lands
would otherwise keep a recovery-labelled transition and timeline.

**Not covered here:**
- Performance reports still list a handover from the last 5 minutes as
resolved.
- The resource drawer labels the change "Alert resolved" before its
reason.
- The incident header in the history expansion still shows its
open/resolved status word.

Both of the first two are filed as a follow-up and noted in
ai-runtime.md and unified-resources.md.

**Tests:**
- `TestOwnershipTransferOnlyClosesWithEvidence`.
- The cause assertions in
`TestReleasedMetricAlertCarriesResolutionInsteadOfRecovery`.
- `TestRecentlyResolvedCarriesHandoverResolution`.
- `TestAlertResolutionWireContract`, which fails without the
`cloneAlert` copy.
- `TestMonitor_HandleAlertLifecycle_HandoverCloseIsNotARecovery`, which
fails on the old code with "Alert resolved: Memory usage at 95%".
- `TestIncidentStore_HandoverCloseIsNotARecovery`.
- `TestSeedHealthAndAlerts_HandoverIsNotARecovery`.
- `TestService_buildAlertContext_HandoverIsNotARecovery`.
- `TestExecuteListResolvedAlertsReportsHandover`.
- Frontend history, presentation and Patrol workbench tests.

The operationaltrust, alerts, models, ai, ai/tools, ai/memory,
unifiedresources, monitoring and notifications suites pass. `tsc`,
eslint and the vitest sweep pass, apart from the Mac-only
`agentInstallCommand` PTY test.

**Browser pass.** A private non-mock stack ran on an alert history
written by the real `releaseCanonicalMetricAlert` handover path. I
checked Alerts history (row, tooltip, expanded timeline) and Patrol's
deep-linked detail at 1280 and 375: no overflow, no console errors.
2026-10-06 19:24:36 +01:00
..
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00