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.InferenceStartedEvent
|
||||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||||
import com.correx.core.events.events.NewEvent
|
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.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
@@ -77,7 +78,15 @@ class SessionEventBridge(
|
|||||||
// actual unresolved approval requests. This happens when approval was resolved but
|
// actual unresolved approval requests. This happens when approval was resolved but
|
||||||
// OrchestrationResumedEvent was not emitted (pre-Feb-13 orchestrator bug). Append the
|
// OrchestrationResumedEvent was not emitted (pre-Feb-13 orchestrator bug). Append the
|
||||||
// missing event permanently so the session state corrects on all future replays.
|
// 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 alreadyResumed = events.any { it.payload is OrchestrationResumedEvent }
|
||||||
val stageId = orchState.currentStageId
|
val stageId = orchState.currentStageId
|
||||||
if (!alreadyResumed && stageId != null) {
|
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.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.RiskAssessedEvent
|
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.ToolExecutionRejectedEvent
|
||||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
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.ToolRequest
|
||||||
import com.correx.core.events.events.TransitionExecutedEvent
|
import com.correx.core.events.events.TransitionExecutedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
@@ -432,29 +429,6 @@ abstract class SessionOrchestrator(
|
|||||||
artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent
|
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 sourceId = toolCall.id ?: invocationId.value
|
||||||
val assistantEntry = ContextEntry(
|
val assistantEntry = ContextEntry(
|
||||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
|
|||||||
Reference in New Issue
Block a user