diff --git a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts index e3768b95c..a188774d3 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts @@ -166,6 +166,51 @@ describe('patrolInvestigationContextModel', () => { expect(handoff.context.handoffContext).not.toContain('online to online'); }); + 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: { @@ -326,6 +371,10 @@ describe('patrolInvestigationContextModel', () => { 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', ); diff --git a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts index eb2cd6ecc..9f07e0d09 100644 --- a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts +++ b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts @@ -354,6 +354,8 @@ const MAX_ASSESSMENT_RESOURCES = 8; const MAX_ASSESSMENT_HANDOFF_ACTIONS = 4; const MAX_PATROL_RUN_HANDOFF_RESOURCES = 8; const MAX_PATROL_BRIEFING_SUGGESTED_PROMPTS = 3; +const MAX_ASSESSMENT_RELATED_RESOURCE_LABELS = 4; +const MAX_ASSESSMENT_RELATED_RESOURCE_LABEL_LENGTH = 80; const SAME_STATE_CHANGED_FIELD_LABELS: Record = { 'docker.command': 'Docker command', 'docker.updateStatus': 'Docker image status', @@ -2065,6 +2067,7 @@ function formatAssessmentRecentChangeEvidence(change: ResourceChange): string | return [ summary, resource ? `resource ${resource}` : undefined, + formatAssessmentRecentChangeRelatedResources(change), observedAt ? `observed ${observedAt}` : undefined, ] .filter(isNonEmptyString) @@ -2085,10 +2088,6 @@ function formatAssessmentCorrelationEvidence(correlation: ResourceCorrelation): } function formatAssessmentRecentChangeContextLine(change: ResourceChange, index: number): string { - const relatedResources = (change.relatedResources ?? []) - .map(normalizeText) - .filter(isNonEmptyString) - .slice(0, 4); const parts = [ formatAssessmentRecentChangeSummary(change), normalizeText(change.id) ? `change ${normalizeText(change.id)}` : undefined, @@ -2105,12 +2104,43 @@ function formatAssessmentRecentChangeContextLine(change: ResourceChange, index: ? `${formatIdentifierLabel(change.confidence)?.toLowerCase()} confidence` : undefined, normalizeText(change.actor) ? `actor ${truncateContextText(change.actor, 80)}` : undefined, - relatedResources.length > 0 ? `related ${relatedResources.join(', ')}` : undefined, + formatAssessmentRecentChangeRelatedResources(change), ].filter(isNonEmptyString); return `Recent Change ${index}: ${parts.join('; ')}`; } +function formatAssessmentRecentChangeRelatedResources( + change: ResourceChange, +): string | undefined { + const labels: string[] = []; + for (const relatedResource of change.relatedResources ?? []) { + const label = truncateContextText( + relatedResource, + MAX_ASSESSMENT_RELATED_RESOURCE_LABEL_LENGTH, + ); + if (label && !labels.includes(label)) { + labels.push(label); + } + } + if (labels.length === 0) return undefined; + + const visibleLabels = labels.slice(0, MAX_ASSESSMENT_RELATED_RESOURCE_LABELS); + const omittedCount = labels.length - visibleLabels.length; + return `related resources ${formatAssessmentRelatedResourceList(visibleLabels, omittedCount)}`; +} + +function formatAssessmentRelatedResourceList(labels: string[], omittedCount: number): string { + if (labels.length === 1) { + return omittedCount > 0 ? `${labels[0]}, and ${omittedCount} more` : labels[0]; + } + if (labels.length === 2 && omittedCount === 0) { + return `${labels[0]} and ${labels[1]}`; + } + const visibleList = labels.join(', '); + return omittedCount > 0 ? `${visibleList}, and ${omittedCount} more` : visibleList; +} + function formatAssessmentCorrelationContextLine( correlation: ResourceCorrelation, index: number,