fix: guard stuck-session fix against OrchestrationPaused/ApprovalRequested race
When a TUI client connects at the exact moment between an OrchestrationPausedEvent being stored and its corresponding ApprovalRequestedEvent being stored, the approval projector sees empty pendingApprovalRequests while orchState.pendingApproval=true. The stuck-session fix would then append a spurious OrchestrationResumedEvent, clearing the TUI's pendingApproval display and hiding the approval dialog. Fix: count OrchestrationPausedEvents vs ApprovalRequestedEvents in the event log. If there is an unpaired pause (more pauses than requests), the session just entered the gate — skip the fix entirely. Also remove spurious ToolExecutionCompleted/Failed emissions from the kernel orchestrator; those events are emitted by SandboxedToolExecutor in the infrastructure layer. Keep ToolExecutionRejectedEvent which the orchestrator does own (rejection happens before executor.execute is called).
This commit is contained in:
@@ -17,6 +17,7 @@ import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
@@ -77,7 +78,15 @@ class SessionEventBridge(
|
||||
// actual unresolved approval requests. This happens when approval was resolved but
|
||||
// OrchestrationResumedEvent was not emitted (pre-Feb-13 orchestrator bug). Append the
|
||||
// missing event permanently so the session state corrects on all future replays.
|
||||
if (orchState.pendingApproval && pendingApprovalRequests.isEmpty()) {
|
||||
//
|
||||
// Guard: if there are more OrchestrationPausedEvents than ApprovalRequestedEvents the
|
||||
// session just entered the approval gate and ApprovalRequestedEvent has not been stored
|
||||
// yet (tiny race window between two sequential emits). Do NOT fire in that case —
|
||||
// appending a spurious resume would hide the pending approval from the TUI.
|
||||
val pauseCount = events.count { it.payload is OrchestrationPausedEvent }
|
||||
val approvalRequestCount = events.count { it.payload is ApprovalRequestedEvent }
|
||||
val hasUnpairedPause = pauseCount > approvalRequestCount
|
||||
if (orchState.pendingApproval && pendingApprovalRequests.isEmpty() && !hasUnpairedPause) {
|
||||
val alreadyResumed = events.any { it.payload is OrchestrationResumedEvent }
|
||||
val stageId = orchState.currentStageId
|
||||
if (!alreadyResumed && stageId != null) {
|
||||
|
||||
-26
@@ -29,11 +29,8 @@ import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.RiskAssessedEvent
|
||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.ToolReceipt
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
@@ -432,29 +429,6 @@ abstract class SessionOrchestrator(
|
||||
artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is ToolResult.Success -> emit(sessionId, ToolExecutionCompletedEvent(
|
||||
invocationId = invocationId,
|
||||
sessionId = sessionId,
|
||||
toolName = toolCall.function.name,
|
||||
receipt = ToolReceipt(
|
||||
invocationId = invocationId,
|
||||
toolName = toolCall.function.name,
|
||||
exitCode = result.exitCode,
|
||||
outputSummary = result.output.take(500),
|
||||
durationMs = 0,
|
||||
tier = tier,
|
||||
timestamp = Clock.System.now(),
|
||||
),
|
||||
))
|
||||
is ToolResult.Failure -> emit(sessionId, ToolExecutionFailedEvent(
|
||||
invocationId = invocationId,
|
||||
sessionId = sessionId,
|
||||
toolName = toolCall.function.name,
|
||||
reason = result.reason,
|
||||
))
|
||||
}
|
||||
|
||||
val sourceId = toolCall.id ?: invocationId.value
|
||||
val assistantEntry = ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
|
||||
Reference in New Issue
Block a user