Close Phase 3 live schedule-policy feedback loop

This commit is contained in:
axiomlogicnexus 2026-05-06 02:39:27 +02:00
parent adf90c7d5c
commit 8c202cba97
4 changed files with 365 additions and 0 deletions

View file

@ -4335,6 +4335,251 @@ FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithQueue
return Brief;
}
FHyperTwistCoachSignal UHyperTwistTrainingCoachLibrary::DeriveSchedulePolicyFeedbackSignal(
const FHyperTwistCoachSchedulePolicySummary& SchedulePolicySummary,
const FHyperTwistTrainingCoachSessionQueueExecutionSummary& QueueExecutionSummary,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,
const FHyperTwistCoachBrief& CurrentBrief)
{
if (!SchedulePolicySummary.IsStructurallyValid() || !QueueExecutionSummary.IsStructurallyValid())
{
return FHyperTwistCoachSignal();
}
const bool bManualRescheduleReview = SchedulePolicySummary.bNeedsManualRescheduleReview;
const bool bArchiveAwareDeferrals = SchedulePolicySummary.bHasArchiveAwareDeferrals;
const bool bBlockedFollowUpDeferrals = SchedulePolicySummary.bHasBlockedFollowUpDeferrals;
const bool bOutstandingDeferredWork = QueueExecutionSummary.bHasOutstandingDeferredWork;
const bool bCompletionGap = QueueExecutionSummary.bHasCompletionGap;
const bool bRepeatManualReschedule = QueueExecutionSummary.bHasRepeatManualRescheduleFriction;
const bool bHighPolicyPressure = SchedulePolicySummary.ReschedulePressureScore >= 25.0f;
const bool bHighExecutionPressure = QueueExecutionSummary.ExecutionPressureScore >= 25.0f;
const bool bNeedsFeedback =
bHighPolicyPressure
|| bHighExecutionPressure
|| bManualRescheduleReview
|| bArchiveAwareDeferrals
|| bBlockedFollowUpDeferrals
|| bOutstandingDeferredWork
|| bCompletionGap
|| bRepeatManualReschedule;
if (!bNeedsFeedback)
{
return FHyperTwistCoachSignal();
}
const bool bHistoryPrefersFollowUp =
SchedulePolicySummary.bFollowUpContinuationFromRecoveryHistory;
const bool bHistoryPrefersRecommended =
SchedulePolicySummary.bBorderlineFollowUpSuppressedByRecoveryHistory;
const bool bPreferFollowUpRecovery =
bBlockedFollowUpDeferrals
|| bHistoryPrefersFollowUp
|| CoachMemorySnapshot.PreferredGuidanceLane
== EHyperTwistTrainingCoachGuidanceLane::FollowUp;
const bool bPreferRecommendedRecovery =
!bPreferFollowUpRecovery
&& (
bHistoryPrefersRecommended
|| CoachMemorySnapshot.PreferredGuidanceLane
== EHyperTwistTrainingCoachGuidanceLane::Recommended
);
const bool bCompetingGuidancePressure =
CoachMemorySnapshot.RecommendedGuidanceContinueBias > 0
&& CoachMemorySnapshot.FollowUpGuidanceContinueBias > 0;
const FString SignalId = bPreferFollowUpRecovery
? TEXT("recover_follow_up_schedule_friction")
: (bPreferRecommendedRecovery
? TEXT("recover_recommended_schedule_friction")
: (bArchiveAwareDeferrals
? TEXT("stabilize_archive_aware_schedule")
: (bManualRescheduleReview
? TEXT("review_manual_schedule_friction")
: (bCompletionGap
? TEXT("recover_schedule_completion_gap")
: TEXT("recover_schedule_friction")))));
const FString Headline =
bBlockedFollowUpDeferrals
? (bCompletionGap
? TEXT("Live queue friction is still blocking follow-up completion")
: TEXT("Live queue friction is still holding follow-up work back"))
: (bManualRescheduleReview
? TEXT("Manual reschedule pressure is still shaping the next coaching run")
: (bArchiveAwareDeferrals
? TEXT("Archive-aware deferrals are still shaping the next coaching run")
: (bCompletionGap
? TEXT("Live queue execution still needs a tighter completion pass")
: TEXT("Deferred queue work still needs schedule recovery"))));
const FString Detail = FString::Printf(
TEXT("policyPressure=%.2f, executionPressure=%.2f, immediate=%d, pending=%d, deferred=%d, manualRescheduleEntries=%d, manualRescheduleEvents=%d, completionGap=%s, outstandingDeferred=%s, blockedFollowUp=%s, archiveAware=%s, dominantDeferral=%s, closure=%s"),
SchedulePolicySummary.ReschedulePressureScore,
QueueExecutionSummary.ExecutionPressureScore,
SchedulePolicySummary.ImmediateEntryCount,
SchedulePolicySummary.PendingEntryCount,
SchedulePolicySummary.DeferredEntryCount,
SchedulePolicySummary.ManualRescheduleEntryCount,
QueueExecutionSummary.ManualRescheduleEventCount,
bCompletionGap ? TEXT("yes") : TEXT("no"),
bOutstandingDeferredWork ? TEXT("yes") : TEXT("no"),
bBlockedFollowUpDeferrals ? TEXT("yes") : TEXT("no"),
bArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"),
SchedulePolicySummary.DominantDeferralReasonLabel.IsEmpty()
? TEXT("n/a")
: *SchedulePolicySummary.DominantDeferralReasonLabel,
SchedulePolicySummary.ClosurePolicyLine.IsEmpty()
? TEXT("n/a")
: *SchedulePolicySummary.ClosurePolicyLine
);
const float Score = FMath::Clamp(
59.0f
+ FMath::Min(SchedulePolicySummary.ReschedulePressureScore * 0.22f, 18.0f)
+ FMath::Min(QueueExecutionSummary.ExecutionPressureScore * 0.18f, 14.0f)
+ (bBlockedFollowUpDeferrals ? 7.0f : 0.0f)
+ (bManualRescheduleReview ? 5.0f : 0.0f)
+ (bArchiveAwareDeferrals ? 4.0f : 0.0f)
+ (bOutstandingDeferredWork ? 5.0f : 0.0f)
+ (bCompletionGap ? 5.0f : 0.0f)
+ (bRepeatManualReschedule ? 4.0f : 0.0f),
0.0f,
100.0f
);
const EHyperTwistCoachFocusLane FocusLane =
bArchiveAwareDeferrals && !bBlockedFollowUpDeferrals
? EHyperTwistCoachFocusLane::Stability
: EHyperTwistCoachFocusLane::Cadence;
const EHyperTwistTrainingDeliveryMode SuggestedMode =
bPreferFollowUpRecovery
? EHyperTwistTrainingDeliveryMode::CoachReviewed
: ((CurrentBrief.UserId == SchedulePolicySummary.UserId
&& CurrentBrief.SuggestedMode != EHyperTwistTrainingDeliveryMode::Timer)
? CurrentBrief.SuggestedMode
: CoachMemorySnapshot.DominantCoachSuggestedMode);
const EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy =
bPreferFollowUpRecovery
? EHyperTwistTrainingSelectionPolicy::Scripted
: ((bCompletionGap || bOutstandingDeferredWork)
? EHyperTwistTrainingSelectionPolicy::Weighted
: ((CurrentBrief.UserId == SchedulePolicySummary.UserId
&& CurrentBrief.SuggestedSelectionPolicy
!= EHyperTwistTrainingSelectionPolicy::Weighted)
? CurrentBrief.SuggestedSelectionPolicy
: CoachMemorySnapshot.DominantCoachSelectionPolicy));
FHyperTwistCoachSignal Signal = HyperTwistTrainingCoachLibraryInternal::MakeSignal(
SignalId,
FocusLane,
Score,
Headline,
Detail,
SuggestedMode,
SuggestedSelectionPolicy
);
Signal.SuggestedDeckId = !CurrentBrief.FocusDeckId.IsEmpty()
? CurrentBrief.FocusDeckId
: SchedulePolicySummary.FocusDeckId;
Signal.SuggestedMethodSegmentId = !CurrentBrief.FocusMethodSegmentId.IsEmpty()
? CurrentBrief.FocusMethodSegmentId
: SchedulePolicySummary.FocusMethodSegmentId;
Signal.SuggestedCaseIds = CurrentBrief.FocusCaseIds.Num() > 0
? CurrentBrief.FocusCaseIds
: (CoachMemorySnapshot.SchedulePressureCaseIds.Num() > 0
? CoachMemorySnapshot.SchedulePressureCaseIds
: CoachMemorySnapshot.DueCaseIds);
HyperTwistTrainingCoachLibraryInternal::ApplyGuidancePreferenceToSignal(
Signal,
bPreferFollowUpRecovery
? EHyperTwistTrainingCoachGuidanceLane::FollowUp
: (bPreferRecommendedRecovery
? EHyperTwistTrainingCoachGuidanceLane::Recommended
: CoachMemorySnapshot.PreferredGuidanceLane),
CoachMemorySnapshot,
bCompetingGuidancePressure
);
return Signal;
}
FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithSchedulePolicyFeedbackSignal(
const FHyperTwistCoachBrief& BaseBrief,
const FHyperTwistCoachSignal& SchedulePolicyFeedbackSignal)
{
if (SchedulePolicyFeedbackSignal.SignalId.IsEmpty())
{
return BaseBrief;
}
FHyperTwistCoachBrief Brief = BaseBrief;
if (!Brief.Signals.ContainsByPredicate(
[&SchedulePolicyFeedbackSignal](const FHyperTwistCoachSignal& Candidate)
{
return Candidate.SignalId == SchedulePolicyFeedbackSignal.SignalId;
}))
{
Brief.Signals.Add(SchedulePolicyFeedbackSignal);
}
HyperTwistTrainingCoachLibraryInternal::SortSignals(Brief.Signals);
const bool bPolicyFacingBrief =
Brief.FocusLane == EHyperTwistCoachFocusLane::Cadence
|| Brief.FocusLane == EHyperTwistCoachFocusLane::Stability
|| Brief.bNeedsCadenceRecovery;
const bool bShouldPromotePolicySignal =
bPolicyFacingBrief
&& SchedulePolicyFeedbackSignal.Score >= Brief.PriorityScore - 3.5f;
if (bShouldPromotePolicySignal)
{
for (FHyperTwistCoachSignal& Signal : Brief.Signals)
{
if (Signal.SignalId == SchedulePolicyFeedbackSignal.SignalId)
{
Signal.Score = FMath::Clamp(
FMath::Max(Signal.Score, Brief.Signals[0].Score + 0.5f),
0.0f,
100.0f
);
Signal.Priority = HyperTwistTrainingCoachLibraryInternal::PriorityFromScore(Signal.Score);
Signal.bNeedsImmediateAttention = Signal.Priority == EHyperTwistCoachPriority::Critical;
break;
}
}
HyperTwistTrainingCoachLibraryInternal::SortSignals(Brief.Signals);
}
if (Brief.Signals.Num() > 0)
{
const FString OriginalDeckId = Brief.FocusDeckId;
const FString OriginalMethodSegmentId = Brief.FocusMethodSegmentId;
const TArray<FString> OriginalCaseIds = Brief.FocusCaseIds;
const FString OriginalPrimaryActionLabel = Brief.PrimaryActionLabel;
HyperTwistTrainingCoachLibraryInternal::ApplyPrimarySignalToBrief(Brief, Brief.Signals[0]);
if (Brief.FocusDeckId.IsEmpty())
{
Brief.FocusDeckId = OriginalDeckId;
}
if (Brief.FocusMethodSegmentId.IsEmpty())
{
Brief.FocusMethodSegmentId = OriginalMethodSegmentId;
}
if (Brief.FocusCaseIds.Num() <= 0)
{
Brief.FocusCaseIds = OriginalCaseIds;
}
if (bShouldPromotePolicySignal
&& Brief.PrimaryActionLabel == SchedulePolicyFeedbackSignal.SignalId
&& (!OriginalPrimaryActionLabel.IsEmpty()
&& (OriginalPrimaryActionLabel.Contains(TEXT("schedule"))
|| OriginalPrimaryActionLabel.Contains(TEXT("recovery"))
|| OriginalPrimaryActionLabel.Contains(TEXT("promote_deferred"))
|| OriginalPrimaryActionLabel.Contains(TEXT("reschedule"))
|| OriginalPrimaryActionLabel.Contains(TEXT("archive")))))
{
Brief.PrimaryActionLabel = OriginalPrimaryActionLabel;
}
}
return Brief;
}
FHyperTwistCoachSignal UHyperTwistTrainingCoachLibrary::DeriveClosureRecoveryHistorySignal(
const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,

View file

@ -4143,6 +4143,21 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews()
ActiveCoachSessionQueueState.QueueId,
ActiveLearnerDeckStateSummary.ReferenceUtc
);
const FHyperTwistCoachSignal SchedulePolicyFeedbackSignal =
UHyperTwistTrainingCoachLibrary::DeriveSchedulePolicyFeedbackSignal(
ActiveCoachSchedulePolicySummary,
ActiveCoachSessionQueueExecutionSummary,
ActiveCoachMemorySnapshot,
ActiveCoachBrief
);
if (!SchedulePolicyFeedbackSignal.SignalId.IsEmpty())
{
ActiveCoachBrief = UHyperTwistTrainingCoachLibrary::RefineCoachBriefWithSchedulePolicyFeedbackSignal(
ActiveCoachBrief,
SchedulePolicyFeedbackSignal
);
ActiveCoachSignals = ActiveCoachBrief.Signals;
}
}
else
{

View file

@ -487,6 +487,20 @@ public:
const FHyperTwistCoachSignal& QueueSuppressionSignal
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSignal DeriveSchedulePolicyFeedbackSignal(
const FHyperTwistCoachSchedulePolicySummary& SchedulePolicySummary,
const FHyperTwistTrainingCoachSessionQueueExecutionSummary& QueueExecutionSummary,
const FHyperTwistTrainingCoachMemorySnapshot& CoachMemorySnapshot,
const FHyperTwistCoachBrief& CurrentBrief
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachBrief RefineCoachBriefWithSchedulePolicyFeedbackSignal(
const FHyperTwistCoachBrief& BaseBrief,
const FHyperTwistCoachSignal& SchedulePolicyFeedbackSignal
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
static FHyperTwistCoachSignal DeriveClosureRecoveryHistorySignal(
const FHyperTwistTrainingCoachActionClosureSummary& ClosureSummary,

View file

@ -0,0 +1,91 @@
# HyperTwist Phase 3 schedule-policy feedback closure packet
Created on `2026-05-06`
Status:
- first-party HyperTwist packet
- bounded coaching-stack closure slice
## Purpose
This packet defines the next logical Phase 3 closure task after the linked-state rebuild closure packet landed.
The open task is:
- fold resolved live coach queue schedule-policy and execution summaries back into coach brief shaping
- close the remaining one-way gap where current queue policy/execution truth is derived, surfaced, and persisted, but does not directly refine the active coach brief in the same refresh pass
It is not:
- a new queue model
- a broad coaching heuristic rewrite
- a dashboard flow redesign
- a donor widening task
## Scope
Bounded lane:
- final first-party schedule-friction feedback closure between resolved coach queue state and coach brief shaping
Required result:
- the subsystem derives the current coach brief first, resolves queue state from that brief, derives live schedule-policy and queue-execution summaries, and then runs a second-pass coach brief refinement from those resolved summaries
- the feedback signal uses current first-party queue policy/execution truth, not only previously persisted memory-derived friction fields
- the resulting brief keeps existing launch/recovery action labels when they are already more specific than the feedback signal id
Out of scope:
- rebuilding queue state more than once per refresh
- changing queue creation heuristics
- changing generated-mode executor behavior
- changing external plugin copies
## Current owned product truth
These pieces are already live in first-party code:
- coach memory snapshot derivation from repository state
- coach brief derivation from learner/review/coach-memory inputs
- queue summary, queue state, queue horizon, schedule policy, and queue execution derivation
- dashboard surfaces for queue policy/execution truth
What was still missing:
- a same-pass feedback loop from the resolved live queue schedule/execution summaries back into the active coach brief
- explicit live schedule-policy pressure participating in coach signal ordering after queue state resolution
## Implementation target
The slice should do the following:
1. derive a live schedule-policy feedback signal from:
- `FHyperTwistCoachSchedulePolicySummary`
- `FHyperTwistTrainingCoachSessionQueueExecutionSummary`
- current coach memory preference state
- current coach brief
2. refine the active coach brief with that signal after queue state, horizon, policy, and execution summaries are resolved
3. preserve existing launch/recovery labels when the prior brief was already carrying a more specific schedule-facing action label
## Suggested file ownership
Primary edit surfaces:
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp`
## Acceptance criteria
- a new second-pass schedule-policy feedback signal is available in first-party coach derivation
- `RefreshRepositoryViews()` refines `ActiveCoachBrief` from resolved queue policy/execution summaries in the same refresh pass
- the full UE build still succeeds
## Validation checklist
1. build `UnrealHyperTwist.sln`
2. confirm the new signal compiles through coach library and subsystem consumers
3. confirm the active coach brief now receives live schedule-policy / execution feedback after queue state resolution
That is the packet.