feat(freestyle): soft-lock steering preemption (Slice 5)

- DecisionKind gains PREEMPT; DecisionJournalState tracks planLocked.
- DefaultDecisionJournalReducer: ExecutionPlanLockedEvent flips planLocked
  (no record); post-lock steering notes recorded as PREEMPT with a
  'PRIORITY DIRECTIVE' summary, pre-lock stay STEERING.
- SessionOrchestrator.buildSteeringNoteEntries: post-lock notes surfaced as
  pinned L0/SYSTEM 'PRIORITY DIRECTIVE' entries ahead of the plan; pre-lock
  keep L2. Graph re-routing intentionally deferred (priority surfacing only).
- Tests: PreemptSteeringJournalTest, SteeringPreemptionTest.
This commit is contained in:
2026-06-08 02:58:21 +04:00
parent 37ac767d1f
commit 4d1b4ffbb6
6 changed files with 314 additions and 5 deletions
@@ -2,6 +2,7 @@ package com.correx.core.journal
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StageFailedEvent
@@ -14,6 +15,7 @@ import com.correx.core.journal.model.DecisionRecord
class DefaultDecisionJournalReducer : DecisionJournalReducer {
override fun reduce(state: DecisionJournalState, event: StoredEvent): DecisionJournalState {
if (event.payload is ExecutionPlanLockedEvent) return state.copy(planLocked = true)
val record = when (val p = event.payload) {
is InitialIntentEvent -> DecisionRecord(
sequence = event.sessionSequence,
@@ -22,8 +24,9 @@ class DefaultDecisionJournalReducer : DecisionJournalReducer {
)
is SteeringNoteAddedEvent -> DecisionRecord(
sequence = event.sessionSequence,
kind = DecisionKind.STEERING,
summary = "User steering: ${p.content}",
kind = if (state.planLocked) DecisionKind.PREEMPT else DecisionKind.STEERING,
summary = if (state.planLocked) "PRIORITY DIRECTIVE (overrides plan): ${p.content}"
else "User steering: ${p.content}",
stageId = p.stageId?.value,
)
is ApprovalDecisionResolvedEvent -> DecisionRecord(
@@ -5,4 +5,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class DecisionJournalState(
val records: List<DecisionRecord> = emptyList(),
val planLocked: Boolean = false,
)
@@ -2,7 +2,7 @@ package com.correx.core.journal.model
import kotlinx.serialization.Serializable
enum class DecisionKind { INTENT, STEERING, APPROVAL, TRANSITION, RETRY, FAILURE, ARTIFACT }
enum class DecisionKind { INTENT, STEERING, PREEMPT, APPROVAL, TRANSITION, RETRY, FAILURE, ARTIFACT }
/**
* One distilled decision. [sequence] is the source event's sessionSequence, used for