fix(kernel,server,workflow): per-stage retry budget, sane freestyle budget, tail /events

Three defects surfaced by a live headless freestyle_planning run (all made a
long execution workflow fail or stall in ways invisible to a REST operator):

1. retryCount was session-global (DefaultOrchestrationReducer): TransitionExecuted
   updated currentStageId but never reset retryCount, while shouldRetry gates on
   it per-stage. An early stage that burned its 3 retries starved every later
   stage of its own — the next stage's first *retryable* failure went terminal.
   Reset retryCount on stage entry; back-edge loops stay bounded by the separate
   refinementIterations counter. Regression test added.

2. Freestyle stages ran at StageConfig's 4096-token default (ExecutionPlanCompiler
   never set a budget) vs 16384-32768 for static role_pipeline stages. A tool-heavy
   stage truncated its context every round, evicting the file_read results it just
   gathered, so it re-read forever and never accumulated enough to write. Set 16384.

3. GET /events returned the OLDEST `limit` events (readFrom(0) then take): for a
   session past `limit` events the tail — including a pending ApprovalRequested —
   was invisible to a headless/REST poller, the exact caller POST /approve serves.
   Default now tails; explicit fromSeq still paginates forward.
This commit is contained in:
2026-07-01 13:41:06 +04:00
parent c8c2521fa2
commit 4be8f292ae
4 changed files with 43 additions and 4 deletions
@@ -1,6 +1,7 @@
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
@@ -8,6 +9,7 @@ import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.orchestration.OrchestrationStatus
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
import com.correx.testing.fixtures.EventFixtures.stored
import org.junit.jupiter.api.Assertions.assertEquals
@@ -133,6 +135,23 @@ class OrchestrationReducerTest {
assertNull(retrying1.failureReason)
}
@Test
fun `TransitionExecutedEvent resets retryCount so each stage gets its own retry budget`() {
val exhausted = reducer.reduce(
state,
stored(payload = RetryAttemptedEvent(sessionId, stageId, 3, 3, "boom")),
)
assertEquals(3, exhausted.retryCount)
val nextStage = StageId("stage-2")
val advanced = reducer.reduce(
exhausted,
stored(payload = TransitionExecutedEvent(sessionId, stageId, nextStage, TransitionId("t1"))),
)
assertEquals(nextStage, advanced.currentStageId)
assertEquals(0, advanced.retryCount)
}
@Test
fun `unrelated event does nothing`() {