# 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<SessionId, ...>`) — 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?
