feat(kernel): emit ContextTruncatedEvent on workflow context overflow

DefaultContextPackBuilder already enforced the token budget, but SessionOrchestrator discarded the entriesDropped signal at both build sites — overflow was enforced yet unobserved. Emit ContextTruncatedEvent when entriesDropped>0, matching the router path (open-threads 6.2 contract).
This commit is contained in:
2026-06-02 13:58:06 +04:00
parent 97ad456778
commit b976a5c92a
2 changed files with 75 additions and 0 deletions
@@ -23,6 +23,7 @@ import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ContextTruncatedEvent
import com.correx.core.events.events.ToolCallAssessedEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
@@ -267,6 +268,7 @@ abstract class SessionOrchestrator(
entries = systemPrompt + schemaEntries + promptEntries + steeringEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
emitContextTruncationIfNeeded(sessionId, stageId, contextPack)
var currentContext = contextPack
var inferenceResult = runInference(
@@ -310,6 +312,7 @@ abstract class SessionOrchestrator(
entries = allEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
emitContextTruncationIfNeeded(sessionId, stageId, currentContext)
inferenceResult = runInference(
sessionId, stageId, currentContext, stageConfig, config.stageTimeoutMs, responseFormat,
)
@@ -937,6 +940,28 @@ abstract class SessionOrchestrator(
// --- event emission ---
// Surface context-budget overflow as an event on the workflow path. The builder already
// enforces the budget (drops oldest/compressible entries, pinning steering + event history);
// this records that it happened so replay and operators can see it, mirroring the router path.
private suspend fun emitContextTruncationIfNeeded(
sessionId: SessionId,
stageId: StageId,
contextPack: ContextPack,
) {
val meta = contextPack.compressionMetadata
if (meta.entriesDropped <= 0) return
emit(
sessionId,
ContextTruncatedEvent(
sessionId = sessionId,
turnId = stageId.value,
entriesDropped = meta.entriesDropped,
truncatedLayers = meta.truncatedLayers.map { it.name },
timestampMs = Clock.System.now().toEpochMilliseconds(),
),
)
}
internal suspend fun emit(sessionId: SessionId, payload: EventPayload) {
log.debug("[session {}] emitting event, payload: {}", sessionId.value.take(7), payload)
eventStore.append(