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 1dd4331e..9492e5b0 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 @@ -42,6 +42,11 @@ import com.correx.core.events.types.SessionId import com.correx.core.kernel.orchestration.OrchestrationRepository import com.correx.core.tools.registry.ToolRegistry +// How many of a session's events the reopen snapshot carries. The full log lives in the event +// store; this bounds the snapshot frame while being generous enough that a normal session's event +// list isn't truncated on reopen (was 7, which dropped most of a 100+ event run). +private const val SNAPSHOT_EVENT_LIMIT = 200 + class SessionEventBridge( private val eventStore: EventStore, private val artifactStore: ArtifactStore, @@ -53,6 +58,16 @@ class SessionEventBridge( ) { private val approvalProjector = ApprovalProjector(DefaultApprovalReducer()) + /** + * Resolves the last stage's inference output from CAS so a reopened session restores its output + * panel. Returns empty if there's no completed inference or the artifact is unavailable. + */ + private suspend fun resolveLastOutput(events: List): String = + events.lastOrNull { it.payload is InferenceCompletedEvent } + ?.let { (it.payload as InferenceCompletedEvent).responseArtifactId } + ?.let { artifactStore.get(it)?.toString(Charsets.UTF_8) } + .orEmpty() + suspend fun replaySnapshot() { val lastGlobal = eventStore.lastGlobalSequence() eventStore.allSessionIds().forEach { sessionId -> @@ -122,7 +137,10 @@ class SessionEventBridge( val recentEvents = events .mapNotNull { eventToEntry(it) } - .takeLast(7) + .takeLast(SNAPSHOT_EVENT_LIMIT) + + // Restore the output panel on reopen (otherwise the snapshot carries no output). + val lastOutput = resolveLastOutput(events) // Re-register pending approvals so the ApprovalCoordinator can route responses // from clients that connected after the ApprovalRequestedEvent was emitted. @@ -143,6 +161,8 @@ class SessionEventBridge( pendingApprovals = pendingApprovals, tools = toolRecords, recentEvents = recentEvents, + lastOutput = lastOutput, + lastResponse = lastOutput, lastSequence = lastGlobal, lastSessionSequence = lastSession, )) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt index 1258cb94..b8395047 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt @@ -119,6 +119,10 @@ sealed interface ServerMessage { val pendingApprovals: List, val tools: List = emptyList(), val recentEvents: List = emptyList(), + // Last stage's inference output, resolved from CAS, so a reopened session restores its + // output panel instead of showing blank until new activity arrives. + val lastOutput: String = "", + val lastResponse: String = "", val lastSequence: Long, val lastSessionSequence: Long, override val sequence: Long? = null, diff --git a/apps/tui-go/internal/app/server.go b/apps/tui-go/internal/app/server.go index 5cf66f7a..30aa5c98 100644 --- a/apps/tui-go/internal/app/server.go +++ b/apps/tui-go/internal/app/server.go @@ -394,6 +394,8 @@ func (m *Model) onSnapshot(msg protocol.ServerMessage) { if msg.State != nil && msg.State.CurrentStageID != nil { sess.CurrentStage = *msg.State.CurrentStageID } + sess.LastOutput = msg.LastOutput + sess.LastResponse = msg.LastResponse for _, e := range msg.RecentEvents { sess.Events = append(sess.Events, EventEntry{formatTime(e.Timestamp), e.Type, e.Detail}) } diff --git a/apps/tui-go/internal/protocol/protocol.go b/apps/tui-go/internal/protocol/protocol.go index 87bd31ac..d661af62 100644 --- a/apps/tui-go/internal/protocol/protocol.go +++ b/apps/tui-go/internal/protocol/protocol.go @@ -67,6 +67,8 @@ type ServerMessage struct { Reason string `json:"reason"` Summary string `json:"outputSummary"` Response string `json:"responseText"` + LastOutput string `json:"lastOutput"` // session_snapshot: last stage output (reopen) + LastResponse string `json:"lastResponse"` // session_snapshot: last stage response (reopen) Content string `json:"content"` Diff *string `json:"diff"` Tier string `json:"tier"`