Files
correx/docs/epics/epic-13-resolution.md
T

11 KiB

Epic 13 — Interfaces (TUI + CLI + API)

completed deliverables

task 0 — orchestrator resume mechanism

fixed the approval gate in SessionOrchestrator to truly suspend and resume rather than immediately failing.

changes:

  • handleApproval is now suspend — holds a CompletableDeferred<ApprovalDecision> per pending request, keyed by ApprovalRequestId
  • emits ApprovalRequestedEvent before suspending so the server layer can surface the request id to clients
  • DefaultSessionOrchestrator.submitApprovalDecision(requestId, decision) completes the deferred and resumes the workflow coroutine
  • ReplayOrchestrator.mapValidationOutcome updated to suspend to match the parent signature
  • approvalEngine removed from SessionOrchestrator (was evaluated synchronously; now decisions arrive externally)

files changed:

  • core/kernel/.../orchestration/SessionOrchestrator.kt
  • core/kernel/.../orchestration/DefaultSessionOrchestrator.kt
  • core/kernel/.../orchestration/ReplayOrchestrator.kt

task 1 — websocket protocol

defined the wire protocol as sealed class hierarchies with kotlinx.serialization in apps/server/protocol/.

final structures:

  • ServerMessage — 16 subtypes across session lifecycle, stage execution, inference, tool execution, approval, provider status, and error
  • ClientMessage — 5 subtypes: StartSession, ResumeSession, CancelSession, ApprovalResponse, Ping
  • ApprovalDecision (protocol enum) — APPROVE, REJECT, STEER — separate from the domain ApprovalDecision data class
  • RiskSummaryDto, ProviderHealthDto, SessionConfigDto, PauseReason — protocol-only DTOs
  • ProtocolSerializer — encodes ServerMessage to JSON, decodes ClientMessage from JSON; unknown/malformed input throws ProtocolException (never crashes)

key properties:

  • Json { classDiscriminator = "type"; ignoreUnknownKeys = true }
  • no domain types leak into the protocol layer except pure value aliases (SessionId, StageId, ApprovalRequestId, Tier)

files created:

  • apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/protocol/ClientMessage.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/protocol/Dtos.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt

task 2 — ktor server + websocket

implemented the full Ktor 3.x server with REST and WebSocket endpoints.

rest endpoints:

  • GET /health — provider status, 200 in degraded mode
  • GET/POST /sessions — list and start sessions
  • GET /sessions/{id} — single session state
  • POST /sessions/{id}/cancel
  • GET /sessions/{id}/events?from=<eventId>&limit=50 — paginated event replay
  • GET /workflows — available workflows
  • GET /providers — provider health

websocket endpoints:

  • WS /sessions/{id}/stream — session-scoped stream
  • WS /stream — global stream across all sessions

websocket behavior:

  • on connect: snapshot of current state, then live events
  • on reconnect with ?lastEventId=<id>: replays missed events from that point
  • 30s server heartbeat; client must respond within 10s or connection is dropped
  • unknown client message → ProtocolError sent, connection kept open

ServerModule holds all dependencies as interfaces; no concrete instantiation inside it.

files created:

  • apps/server/src/main/kotlin/com/correx/apps/server/Application.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/routes/WorkflowRoutes.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/routes/ProviderRoutes.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/registry/WorkflowRegistry.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt

task 3 — approval interaction

implemented the full approval lifecycle bridging the orchestrator suspension and the WebSocket clients.

ApprovalCoordinator responsibilities:

  • tracks connected WS clients per session
  • on ApprovalRequestedEvent: broadcasts ServerMessage.ApprovalRequired to all clients for that session
  • schedules a timeout job (coroutineScope.launch { delay(timeoutMs) }) that auto-denies if no response within ApprovalConfig.timeoutMs (default 5 minutes)
  • on ClientMessage.ApprovalResponse: cancels timeout job, translates protocol enum to domain ApprovalDecision, calls orchestrator.submitApprovalDecision()
  • double-response guard via ConcurrentHashMap<ApprovalRequestId, Boolean> — second response returns ProtocolError, no double-resume

protocol → domain mapping:

  • APPROVEApprovalOutcome.APPROVED
  • REJECTApprovalOutcome.REJECTED
  • STEERApprovalOutcome.APPROVED + UserSteering(text = steeringNote, ...)

rest fallback: POST /sessions/{id}/approve for non-WS clients.

files created:

  • apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalConfig.kt
  • apps/server/src/main/kotlin/com/correx/apps/server/routes/ApprovalRoutes.kt

task 4 — mosaic tui

implemented a new apps/tui Gradle module with a Mosaic-based interactive terminal UI.

architecture:

  • single WS listener coroutine updates mutableStateOf(TuiState) — no polling
  • separate stdin reader coroutine sends KeyEvent to a Channel
  • key handler coroutine dispatches protocol messages to the server
  • exponential-backoff reconnect (1s → 30s max) on server disconnect
  • applyServerMessage pure reducer maps ServerMessage to state updates

layout: status bar / session list (last 5) / active session panel / approval panel (collapsed when no pending approval) / input bar

keybinds: n new, c cancel, a/r/s approve/reject/steer (only active when approval pending), ↑↓ navigate, q quit — inactive binds are no-ops

files created:

  • apps/tui/build.gradle
  • apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/StateReducer.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/ws/TuiWsClient.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/components/ActiveSession.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/components/ApprovalPanel.kt
  • apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt

task 5 — clikt cli

implemented the full apps/cli command suite using Clikt.

commands:

  • correx run --workflow <path> [--session <id>] [--auto-approve]
  • correx session list/resume/cancel/events
  • correx approve <session-id> --decision <approve|reject|steer> [--note]
  • correx status
  • correx provider list

output modes: --json (valid parseable JSON), --quiet (errors only), default (human-readable with ANSI color).

correx run behavior:

  • starts session via POST /sessions, subscribes to WS stream
  • prints stage transitions and tool calls to stdout
  • on ApprovalRequired: interactive TTY prompt if stdin is a terminal; auto-deny with warning if no TTY and --auto-approve absent
  • exit codes: 0 complete, 1 failed, 2 approval denied

CliWsClient wraps the Ktor WebSocket client for stream subscriptions.

files created:

  • apps/cli/src/main/kotlin/com/correx/apps/cli/CorrexCli.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/CliConstants.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/ws/CliWsClient.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/commands/RunCommand.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/commands/SessionCommand.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ApproveCommand.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/commands/StatusCommand.kt
  • apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ProviderCommand.kt

task 6 — shared config

implemented configuration loading in core/config.

schema (~/.config/correx/config.toml, override via $CORREX_CONFIG):

[server]
host = "localhost"
port = 8080

[tui]
theme = "dark"
session_list_limit = 5

[cli]
default_output = "human"

[approval]
timeout_ms = 300000

ConfigLoader.load() — reads from file, returns CorrexConfig() defaults on missing file, logs warning and returns defaults on malformed input. no new library dependencies — manual TOML parser for the required subset (sections + key=value pairs).

files created:

  • core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt
  • core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt

final architecture after Epic 13

apps/
├── server          ← Ktor 3.x, REST + WebSocket, approval coordinator
├── tui             ← Mosaic reactive terminal (new module)
├── cli             ← Clikt commands, WS stream client
└── shared          ← (config lives in core/config)

core/
└── config          ← CorrexConfig, ConfigLoader (new module)

major architectural outcomes

Epic 13 established:

  • real approval pause/resume — orchestrator now truly suspends and resumes externally
  • a wire protocol separating transport from domain (no domain types in protocol layer)
  • a Ktor server as the single point of access — TUI and CLI are pure clients
  • WebSocket event streaming with snapshot-on-connect and missed-event replay
  • approval timeout and double-response safety
  • a Mosaic TUI with reactive state and context-aware keybinds
  • a full Clikt CLI with machine-readable JSON output and correct exit codes
  • shared config via TOML with graceful fallback to defaults

what Epic 13 intentionally does NOT include

not implemented:

  • router context isolation (explicitly deferred — see CLAUDE.md)
  • streaming inference responses (explicitly deferred)
  • parallel agent execution (explicitly deferred)
  • GPU residency scheduling (explicitly deferred)
  • TUI config editor (, keybind present, editor not implemented)
  • correx config --edit command (not in server surface)

final state

Correx now has:

a complete interface layer — server, TUI, and CLI — all backed by a single Ktor WebSocket bus, with approval decisions flowing from any client through to the orchestrator's suspended coroutine and resuming execution cleanly.