diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 8844f27..3531de2 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -750,6 +750,16 @@ namespace HyperTwistCoachDashboardWidgetInternal Bias.bHasSignal = Bias.PreferredKind != EHandoffRecommendationKind::None; return Bias; } + + int32 DeriveMethodDrillLaunchRecoveryBias(const FHandoffHistoryDashboardBias& Bias) + { + return Bias.MethodDrillQueueAcceptedDeltaCount * 2 + + Bias.MethodDrillFollowUpAcceptedDeltaCount * 3 + + Bias.MethodDrillRecommendedAcceptedDeltaCount + + Bias.MethodDrillQueueRecommendationCount + + Bias.MethodDrillFollowUpRecommendationCount * 2 + + Bias.MethodDrillRecommendedRecommendationCount; + } } void UHyperTwistCoachDashboardWidget::NativeConstruct() @@ -911,6 +921,14 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() && (QueueRecoveryWeighting.PreferredActionKind == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery || (!QueueRecoveryWeighting.IsStructurallyValid() && bUpstreamPrefersLaunchRecovery)); + const bool bUseMethodDrillRecoverySignal = + !bHasDeferredFirstSuppressionSignal + && QueueRecoveryWeighting.bHasMethodDrillSignal + && QueueRecoveryWeighting.MethodDrillLaunchBiasCount + >= QueueRecoveryWeighting.MethodDrillPromoteBiasCount + && (QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery + || (!QueueRecoveryWeighting.IsStructurallyValid() && bUpstreamPrefersLaunchRecovery)); auto StartRunAndRefresh = [this](const FHyperTwistTrainingRunState& RunState) { @@ -929,7 +947,8 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() bBeforeOutstandingDeferred, bBeforeCompletionGap, bHasDeferredFirstSuppressionSignal, - bPrefersFollowUp + bPrefersFollowUp, + bUseMethodDrillRecoverySignal ]( const TCHAR* AppliedActionLabel, const FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind ActionKind, @@ -945,11 +964,34 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() bBeforeOutstandingDeferred, bBeforeCompletionGap, bHasDeferredFirstSuppressionSignal && bPrefersFollowUp, + bUseMethodDrillRecoverySignal, bSucceeded ); UpdateDashboardPresentation(); return bSucceeded; }; + auto BuildRecoveryLaunchSourceLabel = + [bHasDeferredFirstSuppressionSignal, bUseMethodDrillRecoverySignal]( + const EHyperTwistTrainingCoachHandoffKind HandoffKind) + { + if (bHasDeferredFirstSuppressionSignal) + { + return HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( + HandoffKind, + true + ); + } + if (bUseMethodDrillRecoverySignal) + { + return HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( + HandoffKind, + false, + true + ); + } + + return FString(); + }; auto TryPromoteRecovery = [&]() -> bool { if (DeferredEntryId.IsEmpty()) @@ -981,7 +1023,8 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( EHyperTwistTrainingCoachHandoffKind::Queue, true) - : FString(); + : BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::Queue); const bool bSucceeded = StartRunAndRefresh( StartNextQueuedCoachRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -996,12 +1039,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() && CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) { - const FString StartActionSourceLabel = - bHasDeferredFirstSuppressionSignal - ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( - EHyperTwistTrainingCoachHandoffKind::FollowUp, - true) - : FString(); + const FString StartActionSourceLabel = BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::FollowUp + ); const bool bSucceeded = StartRunAndRefresh( StartCoachFollowUpRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -1012,12 +1052,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() } if (bPrefersRecommended && CachedCoachPanelState.bCanStartCoachRecommendedRun) { - const FString StartActionSourceLabel = - bHasDeferredFirstSuppressionSignal - ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( - EHyperTwistTrainingCoachHandoffKind::Recommended, - true) - : FString(); + const FString StartActionSourceLabel = BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::Recommended + ); const bool bSucceeded = StartRunAndRefresh( StartCoachRecommendedRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -1033,7 +1070,8 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( EHyperTwistTrainingCoachHandoffKind::Queue, true) - : FString(); + : BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::Queue); const bool bSucceeded = StartRunAndRefresh( StartNextQueuedCoachRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -1044,12 +1082,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() } if (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) { - const FString StartActionSourceLabel = - bHasDeferredFirstSuppressionSignal - ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( - EHyperTwistTrainingCoachHandoffKind::FollowUp, - true) - : FString(); + const FString StartActionSourceLabel = BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::FollowUp + ); const bool bSucceeded = StartRunAndRefresh( StartCoachFollowUpRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -1060,12 +1095,9 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() } if (CachedCoachPanelState.bCanStartCoachRecommendedRun) { - const FString StartActionSourceLabel = - bHasDeferredFirstSuppressionSignal - ? HyperTwistCoachDashboardWidgetInternal::DashboardHandoffSourceLabel( - EHyperTwistTrainingCoachHandoffKind::Recommended, - true) - : FString(); + const FString StartActionSourceLabel = BuildRecoveryLaunchSourceLabel( + EHyperTwistTrainingCoachHandoffKind::Recommended + ); const bool bSucceeded = StartRunAndRefresh( StartCoachRecommendedRun(DefaultCoachMaxCases, StartActionSourceLabel)); return RecordAndFinalizeOutcome( @@ -4619,6 +4651,10 @@ FHyperTwistCoachDashboardQueueRecoveryWeighting UHyperTwistCoachDashboardWidget: FHyperTwistCoachDashboardQueueRecoveryWeighting Weighting; float PromoteDeltaAccumulator = 0.0f; float LaunchDeltaAccumulator = 0.0f; + const HyperTwistCoachDashboardWidgetInternal::FHandoffHistoryDashboardBias HandoffHistoryBias = + HyperTwistCoachDashboardWidgetInternal::DeriveHandoffHistoryDashboardBias( + HandoffHistoryEntries + ); auto ConsumeOutcome = [ &Weighting, @@ -4709,17 +4745,40 @@ FHyperTwistCoachDashboardQueueRecoveryWeighting UHyperTwistCoachDashboardWidget: LaunchDeltaAccumulator / static_cast(Weighting.LaunchCount); } - Weighting.bHasSignal = Weighting.PromoteCount > 0 || Weighting.LaunchCount > 0; + Weighting.MethodDrillLaunchBiasCount = + HyperTwistCoachDashboardWidgetInternal::DeriveMethodDrillLaunchRecoveryBias( + HandoffHistoryBias + ); + if (CachedCoachPanelState.PreferredGuidanceSourceLabel + == TEXT("coach-memory-method-drill-follow-up-strong")) + { + Weighting.MethodDrillLaunchBiasCount += 2; + } + else if ( + CachedCoachPanelState.PreferredGuidanceSourceLabel + == TEXT("coach-memory-method-drill-follow-up-weak")) + { + Weighting.MethodDrillPromoteBiasCount += 2; + } + Weighting.bHasMethodDrillSignal = + Weighting.MethodDrillLaunchBiasCount > 0 || Weighting.MethodDrillPromoteBiasCount > 0; + + const bool bHasDirectOutcomeSignal = Weighting.PromoteCount > 0 || Weighting.LaunchCount > 0; + Weighting.bHasSignal = bHasDirectOutcomeSignal || Weighting.bHasMethodDrillSignal; if (!Weighting.bHasSignal) { return Weighting; } - auto ScoreAction = [](const int32 Count, const int32 ReducedCount, const int32 FailureCount, const float AverageDelta) + auto ScoreAction = [bHasDirectOutcomeSignal]( + const int32 Count, + const int32 ReducedCount, + const int32 FailureCount, + const float AverageDelta) { if (Count <= 0) { - return -1000000.0f; + return bHasDirectOutcomeSignal ? -1000000.0f : 0.0f; } const int32 NeutralCount = FMath::Max(Count - ReducedCount - FailureCount, 0); @@ -4756,9 +4815,13 @@ FHyperTwistCoachDashboardQueueRecoveryWeighting UHyperTwistCoachDashboardWidget: 0)) * 0.5f; const float AdjustedPromoteScore = - PromoteScore + (Weighting.bHasDeferredFirstSignal ? DeferredFirstPromoteScore * 1.5f : 0.0f); + PromoteScore + + (Weighting.bHasDeferredFirstSignal ? DeferredFirstPromoteScore * 1.5f : 0.0f) + + static_cast(Weighting.MethodDrillPromoteBiasCount) * 1.5f; const float AdjustedLaunchScore = - LaunchScore + (Weighting.bHasDeferredFirstSignal ? DeferredFirstLaunchScore * 0.75f : 0.0f); + LaunchScore + + (Weighting.bHasDeferredFirstSignal ? DeferredFirstLaunchScore * 0.75f : 0.0f) + + static_cast(Weighting.MethodDrillLaunchBiasCount) * 0.85f; if (AdjustedPromoteScore > AdjustedLaunchScore) { @@ -5001,6 +5064,7 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( const bool bBeforeOutstandingDeferred, const bool bBeforeCompletionGap, const bool bUsedDeferredFirstSuppressionSignal, + const bool bUsedMethodDrillHandoffSignal, const bool bSucceeded) { QueueRecoveryOutcome.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); @@ -5035,6 +5099,8 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( ); QueueRecoveryOutcome.bUsedDeferredFirstSuppressionSignal = bUsedDeferredFirstSuppressionSignal; + QueueRecoveryOutcome.bUsedMethodDrillHandoffSignal = + bUsedMethodDrillHandoffSignal; bHasQueueRecoveryOutcome = QueueRecoveryOutcome.IsStructurallyValid(); if (bHasQueueRecoveryOutcome) { @@ -6635,6 +6701,13 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : (bUpstreamPrefersLaunchRecovery ? TEXT("prefer launch-recovery") : TEXT("no upstream preference")); + const FString QueueRecoveryMethodDrillStatus = + QueueRecoveryWeighting.bHasMethodDrillSignal + ? (QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery + ? TEXT(" | Drill-driven accepted handoff history is reinforcing launch recovery.") + : TEXT(" | Drill-driven accepted handoff history is present, but promote-deferred recovery still holds.")) + : TEXT(""); const bool bHasQueueRecoveryFriction = CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals || CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals @@ -6643,7 +6716,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() || CachedCoachQueueExecutionSummary.bHasCompletionGap; const FString QueueRecoveryStatusLine = bQueueRecoveryMemorySource ? FString::Printf( - TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s | Upstream: %s | Learned: %s%s"), + TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s | Upstream: %s | Learned: %s%s%s"), *QueueRecoveryLaneLabel, *QueueRecoverySourceLabel, CachedCoachPanelState.PrimaryActionLabel.IsEmpty() @@ -6655,24 +6728,27 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() *QueueRecoveryWeightingLabel, bHasDeferredFirstQueueSuppressionSignal && bQueueRecoveryFollowUp ? TEXT(" | Deferred-first suppression is keeping narrowed follow-up recovery primary.") - : TEXT("") + : TEXT(""), + *QueueRecoveryMethodDrillStatus ) : (bHasQueueRecoveryFriction ? FString::Printf( - TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active | Upstream: %s | Learned: %s%s"), + TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active | Upstream: %s | Learned: %s%s%s"), *QueueRecoveryUpstreamActionLabel, *QueueRecoveryWeightingLabel, bHasDeferredFirstQueueSuppressionSignal ? TEXT(" | Deferred-first suppression is still visible in the live coaching signal.") - : TEXT("") + : TEXT(""), + *QueueRecoveryMethodDrillStatus ) : FString::Printf( - TEXT("Recovery: no queue-memory recovery override is currently active | Upstream: %s | Learned: %s%s"), + TEXT("Recovery: no queue-memory recovery override is currently active | Upstream: %s | Learned: %s%s%s"), *QueueRecoveryUpstreamActionLabel, *QueueRecoveryWeightingLabel, bHasDeferredFirstQueueSuppressionSignal ? TEXT(" | Deferred-first suppression is still visible in the live coaching signal.") - : TEXT("") + : TEXT(""), + *QueueRecoveryMethodDrillStatus )); const FString QueueRecoveryDeferredFirstDetail = bHasDeferredFirstQueueSuppressionSignal @@ -6684,7 +6760,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : *QueueDeferredFirstSuppressionSignal->Detail) : TEXT(""); 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 | Upstream action source: %s%s || Learned promote %d/%d reduced, %d failed, avg delta %.2f | launch %d/%d reduced, %d failed, avg delta %.2f | deferred-first promote/launch reduced %d/%d over %d/%d"), + 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%s || Learned promote %d/%d reduced, %d failed, avg delta %.2f | launch %d/%d reduced, %d failed, avg delta %.2f | deferred-first promote/launch reduced %d/%d over %d/%d | drill-driven promote/launch bias %d/%d"), CachedCoachQueueExecutionSummary.DeferredEventCount, CachedCoachQueueExecutionSummary.PromotedEventCount, CachedCoachQueueExecutionSummary.ManualRescheduleEventCount, @@ -6708,7 +6784,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() QueueRecoveryWeighting.DeferredFirstPromoteReducedCount, QueueRecoveryWeighting.DeferredFirstLaunchReducedCount, QueueRecoveryWeighting.DeferredFirstPromoteCount, - QueueRecoveryWeighting.DeferredFirstLaunchCount + QueueRecoveryWeighting.DeferredFirstLaunchCount, + QueueRecoveryWeighting.MethodDrillPromoteBiasCount, + QueueRecoveryWeighting.MethodDrillLaunchBiasCount ); const bool bHasPrimaryQueueSuppression = CachedCoachSessionQueueSummary.bSuppressedPrimaryQueueCreation @@ -6809,7 +6887,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FString QueueRecoveryOutcomeDetailLine = !bHasQueueRecoveryOutcome || !QueueRecoveryOutcome.IsStructurallyValid() ? TEXT("Outcome detail: apply a queue-recovery action to compare before/after pressure, deferred count, blocked follow-up state, and completion-gap state.") : FString::Printf( - TEXT("Recorded: %s | Pressure %.2f -> %.2f | Deferred %d -> %d | Blocked follow-up %s -> %s | Outstanding deferred %s -> %s | Completion gap %s -> %s"), + TEXT("Recorded: %s | Pressure %.2f -> %.2f | Deferred %d -> %d | Blocked follow-up %s -> %s | Outstanding deferred %s -> %s | Completion gap %s -> %s | deferred-first weighted: %s | drill-weighted: %s"), *QueueRecoveryOutcome.RecordedAtUtc, QueueRecoveryOutcome.BeforePressureScore, QueueRecoveryOutcome.AfterPressureScore, @@ -6820,7 +6898,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() QueueRecoveryOutcome.bBeforeOutstandingDeferred ? TEXT("yes") : TEXT("no"), QueueRecoveryOutcome.bAfterOutstandingDeferred ? TEXT("yes") : TEXT("no"), QueueRecoveryOutcome.bBeforeCompletionGap ? TEXT("yes") : TEXT("no"), - QueueRecoveryOutcome.bAfterCompletionGap ? TEXT("yes") : TEXT("no") + QueueRecoveryOutcome.bAfterCompletionGap ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bUsedDeferredFirstSuppressionSignal ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bUsedMethodDrillHandoffSignal ? TEXT("yes") : TEXT("no") ); const FString PreferredDeferredRecoveryEntryId = FindFirstDeferredQueueEntryId(); const bool bCanApplyQueueRecovery = diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 0196596..285ba9c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -148,6 +148,7 @@ struct FHyperTwistCoachDashboardQueueRecoveryOutcome bool bReducedFriction = false; bool bShiftedFriction = false; bool bUsedDeferredFirstSuppressionSignal = false; + bool bUsedMethodDrillHandoffSignal = false; bool IsStructurallyValid() const { @@ -167,12 +168,15 @@ struct FHyperTwistCoachDashboardQueueRecoveryWeighting int32 DeferredFirstPromoteReducedCount = 0; int32 DeferredFirstLaunchCount = 0; int32 DeferredFirstLaunchReducedCount = 0; + int32 MethodDrillPromoteBiasCount = 0; + int32 MethodDrillLaunchBiasCount = 0; float PromoteAveragePressureDelta = 0.0f; float LaunchAveragePressureDelta = 0.0f; FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind PreferredActionKind = FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::None; bool bHasSignal = false; bool bHasDeferredFirstSignal = false; + bool bHasMethodDrillSignal = false; bool IsStructurallyValid() const { @@ -1241,6 +1245,7 @@ private: bool bBeforeOutstandingDeferred, bool bBeforeCompletionGap, bool bUsedDeferredFirstSuppressionSignal, + bool bUsedMethodDrillHandoffSignal, bool bSucceeded ); void RecordCoachHandoffHistorySnapshot(