diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 0e858d871..158ab3adc 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -170,7 +170,7 @@ runtime cost control, and shared AI transport surfaces. divergence must be sourced from canonical `ReadState.DockerHosts()` views, with model-shaped data limited to the watcher adapter input rather than direct `StateSnapshot.DockerHosts` reads in the Patrol run loop. -4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops. `internal/api/ai_handlers.go` must preserve an explicitly supplied `discovery_interval_hours: 0` as the manual-only setting and may only apply the 24-hour default when discovery is enabled without an explicit interval payload. Discovery analysis remains a Pulse tool-led model workflow: Pulse supplies agent/API/metrics evidence and cache orchestration, while the selected model provides the intelligence. Background, settings-triggered, or drawer-triggered discovery progress must describe that discovery analysis directly and must not imply a live Pulse Assistant chat transcript unless the run is actually executing inside the chat surface. Assistant and Patrol access to discovery must stay behind the governed `pulse_discovery` tool, including an explicit forced refresh action for known resources, while settings-level manual runs must use the canonical `/api/discovery/run` new/changed/stale sweep rather than a frontend-only shortcut. +4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops. `internal/api/ai_handlers.go` must preserve an explicitly supplied `discovery_interval_hours: 0` as the manual-only setting and may only apply the 24-hour default when discovery is enabled without an explicit interval payload. Discovery analysis remains a Pulse tool-led model workflow: Pulse supplies agent/API/metrics evidence and cache orchestration, while the selected model provides the intelligence. Background, settings-triggered, or drawer-triggered discovery progress must describe that discovery analysis directly and must not imply a live Pulse Assistant chat transcript unless the run is actually executing inside the chat surface. Settings-triggered manual discovery is an explicit operator refresh and must not open, append to, or masquerade as a Pulse Assistant session. Assistant and Patrol access to discovery must stay behind the governed `pulse_discovery` tool, including an explicit forced refresh action for known resources, while settings-level manual runs must use the canonical `/api/discovery/run` new/changed/stale sweep rather than a frontend-only shortcut. 5. Preserve auditability for outbound model-bound context exports and keep the export record aligned with the prompt boundary that actually reaches the provider External provider-bound unified-resource context must enforce the same data-handling policy the export audit records: `local-only` resources are diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index cb51e8152..2677a2c64 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -905,8 +905,9 @@ AI runtime. settings pair so selecting "Every 6 hours" or "Manual only" round-trips through `/api/settings/ai` without depending on stale read-side diffing. The same workload-discovery settings section must expose a manual - "Run discovery now" action wired through `/api/discovery/run`, while - resource-drawer discovery remains the forced single-resource refresh path. + "Run discovery now" action wired through `/api/discovery/run` even when + recurring workload discovery is off, while resource-drawer discovery remains + the forced single-resource refresh path. Assistant-only controls inside the shared shell, such as execution permissions and session maintenance, must stay explicitly labeled as Pulse Assistant controls, while Patrol schedule and autonomy diff --git a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx index edd42ce39..996c83463 100644 --- a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx +++ b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx @@ -122,27 +122,30 @@ export const AIRuntimeControlsSection: Component : 'Workload discovery will automatically re-scan resources at this interval'}

-
-

- Runs the new, changed, and stale workload sweep used by the schedule. -

- -
+
+

+ {state.form.discoveryEnabled + ? 'Runs the new, changed, and stale workload sweep used by the schedule.' + : 'Runs a one-time workload discovery refresh without changing the schedule.'} +

+ +
+

{getAISettingsWorkloadDiscoverySummary().text}

diff --git a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx index cfb45cc01..b53acd59f 100644 --- a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx @@ -506,6 +506,35 @@ describe('AISettings workload discovery persistence', () => { }); }); + it('keeps the manual discovery refresh visible when recurring discovery is off', async () => { + getSettingsMock.mockResolvedValue({ + ...baseSettings(), + discovery_enabled: false, + discovery_interval_hours: 0, + }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /Workload Discovery Off/i })).toBeInTheDocument(); + }); + fireEvent.click(screen.getByRole('button', { name: /Workload Discovery Off/i })); + + expect(screen.queryByLabelText('Scan Interval')).not.toBeInTheDocument(); + expect( + screen.getByText('Runs a one-time workload discovery refresh without changing the schedule.'), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /Run discovery now/i })); + + await waitFor(() => { + expect(runDiscoveryRefreshMock).toHaveBeenCalledTimes(1); + expect(notificationSuccessMock).toHaveBeenCalledWith( + 'Discovery refresh finished: 1 workload refreshed.', + ); + }); + }); + it('reports when a manual discovery refresh has no pending work', async () => { getSettingsMock.mockResolvedValue({ ...baseSettings(), diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 40476d166..209884096 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -268,6 +268,9 @@ describe('settings architecture guardrails', () => { expect(aiSettingsStateSource).toContain('discoveryRunRunning'); expect(aiRuntimeControlsSectionSource).toContain('state.handleRunDiscoveryRefresh()'); expect(aiRuntimeControlsSectionSource).toContain('Run discovery now'); + expect(aiRuntimeControlsSectionSource).toContain( + 'Runs a one-time workload discovery refresh without changing the schedule.', + ); expect(aiRuntimeControlsSectionSource).not.toContain("fetch('/api/discovery/run"); }); diff --git a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts index dee2c7afe..c7c8259f1 100644 --- a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts @@ -38,10 +38,10 @@ describe('aiSettingsPresentation', () => { expect(getAISettingsWorkloadDiscoveryHelpContent()).toEqual({ title: 'What is workload discovery?', description: - 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.', + 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse stores that context so Assistant can use it in chat and Patrol can use it during verification.', }); expect(getAISettingsWorkloadDiscoverySummary()).toEqual({ - text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.', + text: 'Workload discovery stores concrete service context for Assistant chat and Patrol verification, so responses and findings can reference real services and commands instead of generic advice.', }); expect(getAISettingsSetupDialogPresentation()).toEqual({ ariaLabel: 'Set up Assistant and Patrol', diff --git a/frontend-modern/src/utils/aiSettingsPresentation.ts b/frontend-modern/src/utils/aiSettingsPresentation.ts index 15bc1c07c..f8d64b141 100644 --- a/frontend-modern/src/utils/aiSettingsPresentation.ts +++ b/frontend-modern/src/utils/aiSettingsPresentation.ts @@ -44,13 +44,13 @@ export function getAISettingsWorkloadDiscoveryHelpContent() { return { title: 'What is workload discovery?', description: - 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.', + 'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse stores that context so Assistant can use it in chat and Patrol can use it during verification.', } as const; } export function getAISettingsWorkloadDiscoverySummary() { return { - text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.', + text: 'Workload discovery stores concrete service context for Assistant chat and Patrol verification, so responses and findings can reference real services and commands instead of generic advice.', } as const; }