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
@@ -199,10 +199,11 @@ private fun Route.getEventsRoute(module: ServerModule) {
get("/events") {
val id = call.parameters["id"]
?: return@get call.respond(HttpStatusCode.BadRequest, "Missing session id")
val fromSeq = call.request.queryParameters["fromSeq"]?.toLongOrNull() ?: 0L
val fromSeqParam = call.request.queryParameters["fromSeq"]?.toLongOrNull()
val fromSeq = fromSeqParam ?: 0L
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: DEFAULT_EVENT_LIMIT
val typeFilter = call.request.queryParameters["type"]
val rows = module.eventStore.readFrom(TypeId(id), fromSeq)
val mapped = module.eventStore.readFrom(TypeId(id), fromSeq)
.asSequence()
.map { stored ->
val payloadJson = eventJson.encodeToJsonElement(EventPayload.serializer(), stored.payload)
@@ -218,8 +219,12 @@ private fun Route.getEventsRoute(module: ServerModule) {
)
}
.filter { typeFilter == null || it.type == typeFilter }
.take(limit)
.toList()
// No explicit fromSeq = a "what's happening now" tail: return the most recent `limit` events
// (a pending ApprovalRequested lives at the tail, so the oldest-200 default hid it from a
// headless/REST operator — the very caller POST /approve exists to serve). Explicit fromSeq
// keeps forward, ascending pagination from that point.
val rows = if (fromSeqParam == null) mapped.takeLast(limit) else mapped.take(limit)
call.respond(rows)
}
}