# spec ## objective Add `:core:router` — a resident conversational facade that maintains isolated L2 memory, routes inference requests to an LLM provider, and emits steering notes, while exposing a WebSocket-protocol interface for chat and steering interaction. ## scope ### in - `SteeringNote` domain object and `SteeringNoteAddedEvent` in `:core:context` / `:core:events` (Task 0) - `RouterState`, `RouterReducer`, `RouterProjector`, `RouterRepository` in `:core:router` (Tasks 1-2) - `RouterContextBuilder` that assembles a budget-constrained `ContextPack` from L2 memory, workflow status, and conversation history (Task 3) - `RouterFacade` that orchestrates state loading, context building, inference routing, and steering emission (Task 4) - `RouterConfig` and `RouterResponse` types in `:core:router` (Task 4) - Protocol layer additions: `ClientMessage.ChatInput`, `ServerMessage.RouterResponseMessage`, and `ChatMode` enum (Task 5) - Infrastructure wiring: `createRouterFacade()` in `InfrastructureModule` and server WebSocket message routing (Task 5) - Deterministic tests for reducer, projector, context builder, and facade ### out - L3 persistence (cross-session memory) - Streaming inference responses - Router-initiated workflow control (pause/cancel) - Router system prompt configuration via `harness.yaml` - Multiple concurrent router instances - TUI changes (handled separately by the TUI module) - GPU residency scheduling - Context compression beyond the router's own L2 entries ## invariants - `RouterState` is always rebuilt from the event log — never persisted independently - `RouterContextPack` never contains raw `EventPayload` objects, artifact content, or tool outputs - The router emits only `SteeringNoteAddedEvent` to the event store — it never emits events that mutate workflow state - In-memory conversation history is session-scoped (`ConcurrentHashMap`) — no cross-session leakage - L0 entries in the router `ContextPack` (system prompt, workflow status) are never dropped under budget pressure - L2 entries (stage summaries) are evicted oldest-first under budget pressure - Replay is environment-independent — no external service calls required for deterministic projection - `:core:router` does not depend on `:core:orchestration` or `:core:kernel` — it reads state from events only - Every new `EventPayload` must be registered in the `eventModule` polymorphic serialization block - Inference requests from the router use the same cancellation semantics as orchestrator inference ## inputs - User chat messages via `ClientMessage.ChatInput` on WebSocket (sessionId, text, mode) - Domain events on the event log: `WorkflowStartedEvent`, `WorkflowCompletedEvent`, `WorkflowFailedEvent`, `OrchestrationPausedEvent`, `OrchestrationResumedEvent`, `StageCompletedEvent`, `StageFailedEvent` - `RouterConfig` — conversation keep-last count and token budget ## outputs - `ServerMessage.RouterResponseMessage` sent back over WebSocket (content, steeringEmitted flag) - `SteeringNoteAddedEvent` appended to the event store when `mode == ChatMode.STEERING` - `RouterState` — observable projection of router knowledge (L2 memory, workflow status, conversation history) ## dependencies - `:core:events` — event primitives, `StoredEvent`, `EventPayload`, `EventStore`, `EventReplayer`, serialization - `:core:context` — `TokenBudget`, `ContextPack`, `CompressionStrategy` - `:core:inference` — `InferenceRouter`, inference provider contract, cancellation semantics - `:core:sessions` — `SessionId`, `StageId`, workflow type primitives - Server WebSocket handler (existing infrastructure) for message dispatch - TUI protocol types (existing) — `PauseReason` referenced by server message routing ## assumptions - `InferenceRouter.route()` returns an inference provider instance that can be called with a `ContextPack` - `EventStore` supports both reading events (for replay) and writing events (for `SteeringNoteAddedEvent`) within the same module - `ContextPack` already supports layered priority with L0/L1/L2 classification and budget-aware eviction - The `SessionConfigDto` type referenced by `ClientMessage.StartSession` already exists and is shared across modules - A `Clock` abstraction (or `Clock.System`) is available for deterministic timestamping - The server WebSocket message router can be extended with a new `when` branch to dispatch `ChatInput` without breaking existing branches - The server already has access to the `InfrastructureModule` component graph where `RouterFacade` will be injected ## open questions - Should `ChatInput.sessionId` be a `SessionId` domain type or remain a `String`? The existing protocol types (`ServerMessage`, `ClientMessage`) already use domain types like `SessionId`, `StageId` — consistency suggests `SessionId` would be preferred - The existing `InputMode` enum in `apps:tui` has variants `ROUTER, NAVIGATE, FILTER, STEER`. Should the new `ChatMode` protocol enum mirror these exactly (e.g. `ROUTER, STEER`), or use a simpler two-variant enum (`CHAT, STEERING`)? - Task 0 adds types to `:core:context` and `:core:events` — are these to be merged before task 1 starts, or is a single PR acceptable? The epic states "complete and merge before starting task 1." - Does the server's `CorrexServer.kt` already have a WebSocket handler structure that can accommodate a `ChatInput` branch, or does the handler file need review to confirm the extension point?