From 42a4f481984b77693f761d80ce99d90bdda87b68 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 19:07:17 +0200 Subject: [PATCH] Derive queue recovery action from coach memory --- .../HyperTwistCoachDashboardWidget.cpp | 40 ++++++++++++---- .../HyperTwistTrainingCoachLibrary.cpp | 47 +++++++++++++++---- .../HyperTwistTrainingRepositoryLibrary.cpp | 46 ++++++++++++++++++ .../HyperTwistTrainingSubsystem.cpp | 3 ++ .../HyperTwistTrainingTypes.h | 22 +++++++++ 5 files changed, 142 insertions(+), 16 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index a068c00..be376d6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -274,6 +274,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() CachedCoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp; const bool bPrefersRecommended = CachedCoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended; + const bool bUpstreamPrefersLaunchRecovery = + CachedCoachPanelState.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; const bool bCanLaunchQueueRecovery = CachedCoachPanelState.bCanStartQueuedRun || (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) @@ -281,8 +284,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() const bool bPreferLaunchBeforePromote = !DeferredEntryId.IsEmpty() && bCanLaunchQueueRecovery - && QueueRecoveryWeighting.PreferredActionKind - == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; + && (QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery + || (!QueueRecoveryWeighting.IsStructurallyValid() && bUpstreamPrefersLaunchRecovery)); auto StartRunAndRefresh = [this](const FHyperTwistTrainingRunState& RunState) { @@ -3858,16 +3862,27 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FString QueueRecoverySourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty() ? TEXT("n/a") : CachedCoachPanelState.PreferredGuidanceSourceLabel; + const FString QueueRecoveryActionSourceLabel = + CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel.IsEmpty() + ? TEXT("n/a") + : CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel; const FHyperTwistCoachDashboardQueueRecoveryWeighting QueueRecoveryWeighting = BuildQueueRecoveryWeighting(); const bool bCanLaunchQueueRecovery = CachedCoachPanelState.bCanStartQueuedRun || (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) || CachedCoachPanelState.bCanStartCoachRecommendedRun; + const bool bUpstreamPrefersPromoteRecovery = + CachedCoachPanelState.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred; + const bool bUpstreamPrefersLaunchRecovery = + CachedCoachPanelState.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; const bool bPreferLaunchRecoveryAction = !FindFirstDeferredQueueEntryId().IsEmpty() && bCanLaunchQueueRecovery - && QueueRecoveryWeighting.PreferredActionKind - == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; + && (QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery + || (!QueueRecoveryWeighting.IsStructurallyValid() && bUpstreamPrefersLaunchRecovery)); const FString QueueRecoveryWeightingLabel = QueueRecoveryWeighting.PreferredActionKind == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred @@ -3876,6 +3891,11 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery ? TEXT("prefer launch-recovery") : TEXT("no learned preference")); + const FString QueueRecoveryUpstreamActionLabel = bUpstreamPrefersPromoteRecovery + ? TEXT("prefer promote-deferred recovery") + : (bUpstreamPrefersLaunchRecovery + ? TEXT("prefer launch-recovery") + : TEXT("no upstream preference")); const bool bHasQueueRecoveryFriction = CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals || CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals @@ -3884,7 +3904,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() || CachedCoachQueueExecutionSummary.bHasCompletionGap; const FString QueueRecoveryStatusLine = bQueueRecoveryMemorySource ? FString::Printf( - TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s | Learned: %s"), + TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s | Upstream: %s | Learned: %s"), *QueueRecoveryLaneLabel, *QueueRecoverySourceLabel, CachedCoachPanelState.PrimaryActionLabel.IsEmpty() @@ -3892,19 +3912,22 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : *CachedCoachPanelState.PrimaryActionLabel, *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedCoachPanelState.SuggestedMode), *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedCoachPanelState.SuggestedSelectionPolicy), + *QueueRecoveryUpstreamActionLabel, *QueueRecoveryWeightingLabel ) : (bHasQueueRecoveryFriction ? FString::Printf( - TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active | Learned: %s"), + TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active | Upstream: %s | Learned: %s"), + *QueueRecoveryUpstreamActionLabel, *QueueRecoveryWeightingLabel ) : FString::Printf( - TEXT("Recovery: no queue-memory recovery override is currently active | Learned: %s"), + TEXT("Recovery: no queue-memory recovery override is currently active | Upstream: %s | Learned: %s"), + *QueueRecoveryUpstreamActionLabel, *QueueRecoveryWeightingLabel )); const FString QueueRecoveryDetailLine = FString::Printf( - TEXT("Deferred events %d | Promoted %d | Manual %d | Pressure %.2f | Dominant reason: %s | Follow-up deferrals: %s | Archive-aware deferrals: %s | Completion gap: %s || Learned promote %d/%d reduced, %d failed, avg delta %.2f | launch %d/%d reduced, %d failed, avg delta %.2f"), + TEXT("Deferred events %d | Promoted %d | Manual %d | Pressure %.2f | Dominant reason: %s | Follow-up deferrals: %s | Archive-aware deferrals: %s | Completion gap: %s | Upstream action source: %s || Learned promote %d/%d reduced, %d failed, avg delta %.2f | launch %d/%d reduced, %d failed, avg delta %.2f"), CachedCoachQueueExecutionSummary.DeferredEventCount, CachedCoachQueueExecutionSummary.PromotedEventCount, CachedCoachQueueExecutionSummary.ManualRescheduleEventCount, @@ -3915,6 +3938,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals ? TEXT("yes") : TEXT("no"), CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"), CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no"), + *QueueRecoveryActionSourceLabel, QueueRecoveryWeighting.PromoteReducedCount, QueueRecoveryWeighting.PromoteCount, QueueRecoveryWeighting.PromoteFailureCount, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index 9ef3036..b2849f0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -352,6 +352,12 @@ TArray UHyperTwistTrainingCoachLibrary::DeriveCoachSigna const bool bQueueRecoveryRecommended = bQueueRecoverySource && CoachMemorySnapshot.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended; + const bool bPromoteQueueRecovery = + CoachMemorySnapshot.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred; + const bool bLaunchQueueRecovery = + CoachMemorySnapshot.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; const float Score = 66.0f + FMath::Min(CoachMemorySnapshot.ScheduleExecutionPressureScore * 0.25f, 20.0f) + (bArchiveAware ? 5.0f : 0.0f) @@ -364,24 +370,47 @@ TArray UHyperTwistTrainingCoachLibrary::DeriveCoachSigna ? EHyperTwistCoachFocusLane::Stability : EHyperTwistCoachFocusLane::Cadence; const FString Headline = bQueueRecoveryFollowUp - ? TEXT("Recover the deferred follow-up lane before the queue drifts further") + ? (bPromoteQueueRecovery + ? TEXT("Promote the deferred follow-up lane back into active queue flow") + : TEXT("Recover the deferred follow-up lane before the queue drifts further")) : (bQueueRecoveryRecommended - ? TEXT("Recover the deferred recommended lane and restore queue cadence") + ? (bPromoteQueueRecovery + ? TEXT("Promote the deferred recommended lane back into the active queue") + : TEXT("Recover the deferred recommended lane and restore queue cadence")) : (bBlockedFollowUp - ? TEXT("Follow-up scheduling friction is stalling the queue") + ? (bPromoteQueueRecovery + ? TEXT("Promote deferred work to relieve follow-up scheduling friction") + : TEXT("Follow-up scheduling friction is stalling the queue")) : (bArchiveAware ? TEXT("Archive-aware scheduling friction needs cleanup") - : TEXT("Coaching queue scheduling needs intervention")))); + : (bLaunchQueueRecovery + ? TEXT("Launch recovery work to restore coaching queue cadence") + : TEXT("Coaching queue scheduling needs intervention"))))); const FString DeferralReasonLabel = !CoachMemorySnapshot.DominantScheduleDeferralReasonLabel.IsEmpty() ? CoachMemorySnapshot.DominantScheduleDeferralReasonLabel : TEXT("none"); const FString RecoverySourceLabel = bQueueRecoverySource ? CoachMemorySnapshot.PreferredGuidanceSourceLabel : TEXT("none"); + const FString RecoveryActionLabel = + CoachMemorySnapshot.PreferredQueueRecoveryAction == EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred + ? TEXT("promote-deferred") + : (CoachMemorySnapshot.PreferredQueueRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery + ? TEXT("launch-recovery") + : TEXT("none")); + const FString RecoveryActionSourceLabel = + CoachMemorySnapshot.PreferredQueueRecoveryActionSourceLabel.IsEmpty() + ? TEXT("none") + : CoachMemorySnapshot.PreferredQueueRecoveryActionSourceLabel; const FString SignalId = bQueueRecoveryFollowUp - ? TEXT("schedule_friction_follow_up_recovery") + ? (bPromoteQueueRecovery + ? TEXT("schedule_friction_follow_up_promote_recovery") + : TEXT("schedule_friction_follow_up_recovery")) : (bQueueRecoveryRecommended - ? TEXT("schedule_friction_recommended_recovery") + ? (bPromoteQueueRecovery + ? TEXT("schedule_friction_recommended_promote_recovery") + : TEXT("schedule_friction_recommended_recovery")) : TEXT("schedule_friction")); const EHyperTwistTrainingDeliveryMode SuggestedMode = bQueueRecoveryFollowUp ? EHyperTwistTrainingDeliveryMode::CoachReviewed @@ -404,14 +433,16 @@ TArray UHyperTwistTrainingCoachLibrary::DeriveCoachSigna Score, Headline, FString::Printf( - TEXT("deferred=%d, manual=%d, archiveAware=%d, blockedFollowUp=%d, completionGap=%d, deferralReason=%s, recoverySource=%s"), + TEXT("deferred=%d, manual=%d, archiveAware=%d, blockedFollowUp=%d, completionGap=%d, deferralReason=%s, recoverySource=%s, recoveryAction=%s, recoveryActionSource=%s"), CoachMemorySnapshot.ScheduleDeferredEventCount, CoachMemorySnapshot.ScheduleManualRescheduleEventCount, CoachMemorySnapshot.ScheduleArchiveAwareEventCount, CoachMemorySnapshot.ScheduleBlockedFollowUpEventCount, CoachMemorySnapshot.bHasQueueExecutionCompletionGap ? 1 : 0, *DeferralReasonLabel, - *RecoverySourceLabel), + *RecoverySourceLabel, + *RecoveryActionLabel, + *RecoveryActionSourceLabel), SuggestedMode, SuggestedSelectionPolicy); Signal.SuggestedDeckId = CoachMemorySnapshot.FocusDeckId; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index ca6d88b..0db64ac 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -5217,6 +5217,43 @@ FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingRepositoryLibrary::Der DominantDeferredLane == EHyperTwistTrainingCoachGuidanceLane::Recommended || (DominantCompletedLane == EHyperTwistTrainingCoachGuidanceLane::Recommended && QueueExecutionSummary.PromotedEventCount > 0); + const float PromoteRecoveryActionPressure = + (QueueExecutionSummary.bHasOutstandingDeferredWork ? 4.0f : 0.0f) + + FMath::Min(static_cast(QueueExecutionSummary.DeferredEventCount) * 0.75f, 3.0f) + + FMath::Min(static_cast(QueueExecutionSummary.PromotedEventCount), 3.0f) + + (DominantDeferredLane != EHyperTwistTrainingCoachGuidanceLane::None ? 1.5f : 0.0f) + - (QueueExecutionSummary.bHasCompletionGap ? 1.0f : 0.0f); + const float LaunchRecoveryActionPressure = + (QueueExecutionSummary.bHasCompletionGap ? 4.0f : 0.0f) + + (QueueExecutionSummary.bHasBlockedFollowUpFriction ? 2.5f : 0.0f) + + (QueueExecutionSummary.bHasRepeatManualRescheduleFriction ? 2.0f : 0.0f) + + (QueueExecutionSummary.bHasArchiveAwareFriction ? 1.0f : 0.0f) + + FMath::Min(static_cast(QueueExecutionSummary.CompletedEventCount), 3.0f) + + FMath::Min(static_cast(QueueExecutionSummary.ManualRescheduleEventCount) * 0.5f, 2.0f); + if (PromoteRecoveryActionPressure > 0.0f || LaunchRecoveryActionPressure > 0.0f) + { + if (PromoteRecoveryActionPressure > LaunchRecoveryActionPressure) + { + Snapshot.PreferredQueueRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred; + Snapshot.PreferredQueueRecoveryActionSourceLabel = TEXT("queue-history-promote-pressure"); + } + else if (LaunchRecoveryActionPressure > PromoteRecoveryActionPressure) + { + Snapshot.PreferredQueueRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; + Snapshot.PreferredQueueRecoveryActionSourceLabel = TEXT("queue-history-launch-pressure"); + } + else + { + Snapshot.PreferredQueueRecoveryAction = + QueueExecutionSummary.bHasOutstandingDeferredWork + && !QueueExecutionSummary.bHasCompletionGap + ? EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred + : EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; + Snapshot.PreferredQueueRecoveryActionSourceLabel = TEXT("queue-history-balanced-pressure"); + } + } if (bQueueHistoryPrefersFollowUp) { @@ -5364,6 +5401,15 @@ FHyperTwistTrainingCoachMemorySnapshot UHyperTwistTrainingRepositoryLibrary::Der : EHyperTwistTrainingCoachGuidanceLane::FollowUp; Snapshot.PreferredGuidanceSourceLabel = TEXT("coach-memory-default"); } + if (Snapshot.PreferredQueueRecoveryAction == EHyperTwistTrainingQueueRecoveryAction::None + && Snapshot.bNeedsScheduleFrictionRecovery) + { + Snapshot.PreferredQueueRecoveryAction = Snapshot.bHasOutstandingDeferredScheduleWork + && !Snapshot.bHasQueueExecutionCompletionGap + ? EHyperTwistTrainingQueueRecoveryAction::PromoteDeferred + : EHyperTwistTrainingQueueRecoveryAction::LaunchRecovery; + Snapshot.PreferredQueueRecoveryActionSourceLabel = TEXT("coach-memory-schedule-default"); + } return Snapshot; } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index fe8db08..f8ec442 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -322,6 +322,9 @@ FHyperTwistTrainingCoachPanelState UHyperTwistTrainingSubsystem::GetActiveCoachP PanelState.SignalCount = ActiveCoachSignals.Num(); PanelState.PreferredGuidanceLane = ActiveCoachMemorySnapshot.PreferredGuidanceLane; PanelState.PreferredGuidanceSourceLabel = ActiveCoachMemorySnapshot.PreferredGuidanceSourceLabel; + PanelState.PreferredQueueRecoveryAction = ActiveCoachMemorySnapshot.PreferredQueueRecoveryAction; + PanelState.PreferredQueueRecoveryActionSourceLabel = + ActiveCoachMemorySnapshot.PreferredQueueRecoveryActionSourceLabel; PanelState.bRepositoryVerificationPassed = ActiveRepositoryRoundTripVerification.bDidRoundTripPass; PanelState.bRepositoryVerificationNeedsAttention = ActiveRepositoryRoundTripVerification.ResolvedPath.IsEmpty() || !ActiveRepositoryRoundTripVerification.bDidRoundTripPass; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 492e570..99dbd4e 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -32,6 +32,14 @@ enum class EHyperTwistTrainingCoachGuidanceLane : uint8 FollowUp UMETA(DisplayName = "Follow-Up") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingQueueRecoveryAction : uint8 +{ + None UMETA(DisplayName = "None"), + PromoteDeferred UMETA(DisplayName = "Promote Deferred"), + LaunchRecovery UMETA(DisplayName = "Launch Recovery") +}; + UENUM(BlueprintType) enum class EHyperTwistTrainingPromptKind : uint8 { @@ -1837,6 +1845,13 @@ struct FHyperTwistTrainingCoachMemorySnapshot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString PreferredGuidanceSourceLabel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingQueueRecoveryAction PreferredQueueRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PreferredQueueRecoveryActionSourceLabel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 RecommendedGuidancePlanCount = 0; @@ -4281,6 +4296,13 @@ struct FHyperTwistTrainingCoachPanelState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString PreferredGuidanceSourceLabel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingQueueRecoveryAction PreferredQueueRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::None; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PreferredQueueRecoveryActionSourceLabel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 SignalCount = 0;