From 92f20dafdbece689a67dcf22994ff37d81d39a93 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:43:57 +0100 Subject: [PATCH] Hide Re-investigate while a planned fix is queued A Patrol investigation can plan a fix and then fail, normally on the final summary call. The run then shows a queued fix with a failed status, and the Investigation section offered Re-investigate because it only looked at the status. Another run would plan a second action for the same problem while the first still waits for approval. The section now hides Re-investigate while the outcome is fix_queued, as it already did for a completed run with a queued fix. The failed run's note uses the muted style of an interrupted run, because the fix awaiting approval is what the operator acts on, not the error. The Pro orchestrator keeps the plan's outcome on such a run and refuses the re-investigation request too. --- .../subsystems/patrol-intelligence.md | 15 ++++++++++++ .../patrol/InvestigationSection.tsx | 10 +++++--- .../__tests__/InvestigationSection.test.tsx | 23 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index bcfec9a7f7..3a6e1f4648 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -2817,3 +2817,18 @@ now starts a targeted Patrol run, so alert follow-up lands on the Patrol page. `patrolInvestigationContextModel.ts` shrank to the investigation-record presentation that `InvestigationSection` uses, and the Patrol tool transcript block moved to `frontend-modern/src/components/patrol/ToolExecutionBlock.tsx`. + +### Failed run with a planned fix (23 October 2026) + +An investigation can persist a canonical action plan and then fail, normally +on the final summary call. The orchestrator (in pulse-pro) keeps the plan's +outcome, normally `fix_queued`, on the failed run instead of `timed_out` or +`interrupted`, so the retry gates leave the finding alone rather than plan the +same fix again, and it refuses a manual re-investigation while the latest +run's outcome is `fix_queued` (409 `action_pending`). The Investigation section +matches: it offers no Re-investigate while the outcome is `fix_queued`, +whatever the run status, and the failed run's note renders as information, as +for an interrupted run, because the queued fix is what the operator acts on. +Once core reconciles the plan's approval, rejection or expiry, the outcome +changes and the usual rules apply again. Proof: +`frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx`. diff --git a/frontend-modern/src/components/patrol/InvestigationSection.tsx b/frontend-modern/src/components/patrol/InvestigationSection.tsx index 4c3ae711c7..c132a4e2c7 100644 --- a/frontend-modern/src/components/patrol/InvestigationSection.tsx +++ b/frontend-modern/src/components/patrol/InvestigationSection.tsx @@ -44,10 +44,11 @@ const INVESTIGATION_BADGE_PROPS = { const summaryClass = 'text-sm prose prose-slate prose-sm dark:prose-invert max-w-none wrap-break-word prose-headings:my-2 prose-p:my-2 prose-pre:overflow-x-auto prose-code:break-all prose-code:before:content-none prose-code:after:content-none'; -// A run cut short by a restart did not fail: Patrol runs it again, so its note -// reads as information rather than as an error. +// A run cut short by a restart did not fail: Patrol runs it again. A run that +// stopped after planning a fix left that fix waiting for approval. Either way, +// its note reads as information rather than as an error. const runNoteClass = (outcome: string | undefined): string => - outcome === 'interrupted' + outcome === 'interrupted' || outcome === 'fix_queued' ? 'border-border-subtle bg-surface text-muted' : 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-900/25 dark:text-red-300'; @@ -102,6 +103,9 @@ export const InvestigationSection: Component = (props const inv = investigation(); if (!inv) return true; // No investigation yet if (inv.status === 'running') return false; + // A queued fix is still open, even after a run that failed later. + // Investigating again would plan a second action for the same problem. + if (inv.outcome === 'fix_queued') return false; return ( inv.status === 'failed' || inv.status === 'needs_attention' || diff --git a/frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx b/frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx index 29928d257b..1284332cf5 100644 --- a/frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx +++ b/frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx @@ -163,6 +163,29 @@ describe('InvestigationSection', () => { expect(screen.getByRole('button', { name: 'Re-investigate' })).toBeInTheDocument(); }); + it('does not offer another run while a planned fix is queued', async () => { + getInvestigationMock.mockResolvedValue({ + id: 'inv-queued', + finding_id: 'finding-queued', + session_id: 'session-queued', + status: 'failed', + outcome: 'fix_queued', + started_at: '2026-10-23T10:00:00.000Z', + turn_count: 6, + error: 'The provider stopped responding.', + } satisfies Investigation); + + render(() => ); + + // The run failed after planning a fix; the fix is still waiting for approval. + await screen.findByText(getInvestigationOutcomeLabel('fix_queued')); + expect(screen.queryByRole('button', { name: 'Re-investigate' })).not.toBeInTheDocument(); + // The run's note is information beside the queued fix, not an alarm. + const noteBox = screen.getByText('The provider stopped responding.'); + expect(noteBox.className).toContain('text-muted'); + expect(noteBox.className).not.toContain('bg-red-50'); + }); + it('keeps a failed run error in the error style', async () => { getInvestigationMock.mockResolvedValue({ id: 'inv-failed',