diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/bridge/SessionEventBridge.kt b/apps/server/src/main/kotlin/com/correx/apps/server/bridge/SessionEventBridge.kt index 850b8101..67476699 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/bridge/SessionEventBridge.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/bridge/SessionEventBridge.kt @@ -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) { diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 593cab7f..cbd294cf 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -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()),