Reordered handleStartSession so protocol messages are sent before the
orchestrator launches in module.moduleScope. If the WS is already closed,
session.send() throws and the orchestrator never launches — avoiding silent
data loss where the session runs but the client never learns it exists.
- Stop silently booting user from session view on completion
(SessionsReducer.processSessionCompletedMessage no longer deselects)
- Wrap StageToolManifest send in runCatching to survive WS disconnect
after orchestrator launch in module scope
- Fix navigateUp dead state: wrap to last workflow instead of -1
- Consolidate event store reads in SessionEventBridge (single read
for both recentEvents and approval state), removing
DefaultApprovalRepository dependency
- Add unified diff viewer for file_write/file_edit tools across all layers:
ToolReceipt.diff → DiffUtil (LCS-based) → SandboxedToolExecutor → ToolCompleted
→ TuiToolRecord.diff → DiffViewer widget (colored +/- lines)
- Add workflow selection panel: WorkflowList server message, WorkflowListPanel
widget, ↑↓ seamlessly extends into workflow picker from session list
- Add cursor/caret support: inputCursor in TuiState, CursorLeft/CursorRight
actions and key events, AppendChar inserts at cursor, Backspace deletes before
- Colorize event history strip (green/red/yellow/blue by type) and input bar
(blue/yellow prompt, cyan session name, blue keybindings)
- Show 5 recent events instead of 3; increase event strip height to 6
- Remove lastEventAt sort so display and navigation use consistent order
- Fix approval reconnect: register pending approvals with ApprovalCoordinator
during snapshot replay so lookupSession works after reconnect
- 167 tests pass (126 TUI + 41 server)
- Change EventHistoryStrip take(3) to takeLast(3) so it shows the 3 most
recent events instead of the 3 oldest (chronological order is oldest-first)
- Add bridge test confirming replaySnapshot populates recentEvents from the
event store (with 3 different event types mapped correctly)
- Add TUI SessionsReducer test confirming processSessionSnapshotMessage maps
EventEntryDto entries from the snapshot into TuiEventEntry on the summary
Three fixes:
1. Status bar now shows model/provider from ProviderStatusChanged:
- RootReducer sets currentModel and providerType when the message arrives
- providerType is LOCAL for llama/local providers, REMOTE otherwise
- Removes the stale '(no model) (local)' display
2. SessionSnapshot now carries recent event history:
- New EventEntryDto in the protocol with timestamp/type/detail
- SessionSnapshot.recentEvents populated by bridge from event store
- Bridge reads last 7 display-relevant events per session
- TUI processSessionSnapshotMessage maps them to TuiEventEntry
- Completed sessions finally show their event history
3. SessionStarted now switches selectedId to the new session:
- RootReducer cross-field weave updates selectedId alongside sessionEntered
- Previously kept the old selectedId, navigating user into the wrong session
- Works whether starting a session from the list or during replay
Restructure the TUI around a DisplayState enum (IDLE, IN_SESSION, APPROVAL) replacing
the previous InputMode-based navigation (NAVIGATE, STEER removed). Key changes:
- **DisplayState model**: New `DisplayState` enum governs per-mode key resolution and UI
rendering. Removes `ROUTER`/`NAVIGATE`/`STEER`/`FILTER` InputMode complexity.
- **Input history navigation**: Up/Down arrows in IN_SESSION cycle through per-session
input history (ARCH-HISTORY-2/3). `savedInputBuffer` preserves in-flight text.
- **Input history filter**: `Tab` toggles between ROUTER and FILTER modes; history nav
disabled while filtering.
- **Ctrl-C → Cancel**: Rebind Ctrl-C from Quit to Cancel across all states.
- **Approval flow**: `ApproveActive`/`RejectActive` moved from SessionsReducer to
RootReducer via `pendingDecision`/`SubmitApprovalDecision`. Approval overlay
replaced by `ApprovalSurface` component.
- **Event history strip**: Replaces old overlay with a persistent `EventHistoryStrip`
component, toggled via Ctrl-E.
- **Background update badge**: New `backgroundUpdateCount` on SessionsState tracks
events arriving for non-selected sessions (ARCH-BADGE-1).
- **StageToolManifest**: New `ServerMessage.StageToolManifest` and `ToolManifestEntry`
model for pre-declared tool shapes per stage (RF-3).
- **Cleanup**: Removed `ActiveSession`, `ApprovalPanel`, `ToolsPanel` components.
Removed `InputMode.NAVIGATE`, `InputMode.STEER`, approval overlay state fields.
Various reducer simplifications (no more overlaid effects for approval responses).
Two follow-ups for the approval refactor:
- InputReducer STEER input mode hardcodes APPROVE; needs the future
approve/reject overlay UX to let the user pick.
- GlobalStreamHandler derives scopeSessionId from requestId as a
placeholder; needs a real requestId → sessionId lookup once exposed.
ApprovalCoordinator (added in epic-13) was never instantiated. Its four
capabilities — multi-client fan-out, single-resolver dedupe, request
timeout, and a unified domain mapper — were duplicated piecemeal across
GlobalStreamHandler and SessionStreamHandler, with a STEER-outcome
divergence between them (coordinator mapped STEER → APPROVED while
GlobalStreamHandler mapped STEER → REJECTED). This refactor wires the
coordinator and removes the duplication.
Protocol: drop ApprovalDecision.STEER. STEER is no longer a discriminator;
it's expressed as (APPROVE | REJECT) + non-null steeringNote, so the user
can steer alongside either choice. TUI's current single-keybind STEER
input flow now sends APPROVE + note (matching the original epic-13
intent); a future overlay UX will let the user pick APPROVE or REJECT
explicitly.
Mapper: ClientMessage.ApprovalResponse → DomainApprovalDecision lives in
a single shared `toDomain` helper (apps/server/approval/
ApprovalResponseMapper.kt). Both stream handlers and the coordinator
use it.
Config: delete the duplicate apps/server/approval/ApprovalConfig.kt;
ApprovalCoordinator now takes core.config.ApprovalConfig (already had
timeoutMs=300_000L). ConfigLoader already populates it.
Wiring:
- ServerModule owns a SupervisorJob-backed moduleScope, constructs
ApprovalCoordinator, and on start() subscribes eventStore.subscribeAll()
to route ApprovalRequestedEvent payloads to onApprovalRequested.
- GlobalStreamHandler delegates ApprovalResponse to
approvalCoordinator.handleResponse and registers/unregisters via the
new registerGlobalClient/unregisterGlobalClient (global sockets aren't
bound to one SessionId, so broadcasts union session and global sets).
- SessionStreamHandler delegates ApprovalResponse and registers per-session
via registerClient/unregisterClient.
- Timeout is on by default at 5 minutes via config.timeoutMs.
Tests: ApprovalCoordinatorWiringTest covers delegate path, dedupe
producing ProtocolError, and event-stream subscription routing.
Complete the tui-02 spec acceptance criteria: delete the orphan
`connectSession`, `disconnectSession`, `sessionStreamJob`, and
`sessionWsSession` members from TuiWsClient, and remove the `send()`
fallback that previously routed ApprovalResponse over a per-session
socket. All ClientMessage types now travel the single global socket.
Server side: GlobalStreamHandler.handleClientMessage now actually
handles ApprovalResponse (was returning a protocol error), converting
to a domain ApprovalDecision and dispatching to the orchestrator —
mirrors SessionStreamHandler's prior logic.
Adds ApprovalResponseGlobalSocketRoundTripTest covering APPROVE and
STEER encode/decode through ProtocolSerializer.
Close the narrow race where `streamGlobal` launched the subscribeAll
collector and immediately read `lastGlobalSequence()` — events appended
in that window were dropped by the replay=0 SharedFlow before the
collector actually subscribed. Now use a CompletableDeferred barrier
signaled via `onSubscription` (SharedFlow) or `onStart` (cold fallback),
awaited before `bridge.replaySnapshot()`.
Closes the remaining gap on finding #1 from
docs/specs/2026-05-24-review-fixes.
Establishes subscribeAll() before replaySnapshot() so no event can slip between the
projection read and live forwarding. Removes liveStreamJobs map and per-session forwarder
spawn from handleStartSession; all live forwarding flows through the single global
subscription. Closes findings #1 (race) and #5 (truth sources).
Tests cover happy path, race (x100), empty sessions, and shutdown/leak cases.
Capture lastGlobalSequence() before any projection read to anchor the
buffer-drain window; capture lastSequence(sessionId) before each session's
projection read. SessionSnapshot.lastSequence now carries the global cursor
shared across all sessions in the batch. pendingApprovals populated from
filtered approval state and sorted by (timestamp, requestId) for deterministic
ordering. SnapshotComplete emitted unconditionally including the zero-sessions
case. Four new tests: zero-sessions, SnapshotComplete-last ordering, global
cursor isolation, and per-session cursor value.
Adds a file-private SLF4J logger and replaces the silent else->null branch
with a debug log line that names the payload type, sessionId, and sequence.
Test extended with a log-capture case that verifies exactly one DEBUG event
is emitted containing the payload class name for unrecognised event types.
StoredEvent gains sessionSequence: Long (per-session monotonic); existing
sequence field becomes the global monotonic cursor across the entire store.
SqliteEventStore allocates both counters inside the same BEGIN IMMEDIATE
transaction and emits to a global MutableSharedFlow after commit.
InMemoryEventStore mirrors this with an AtomicLong global counter and a
suspend-on-overflow SharedFlow. EventStore interface gains subscribeAll()
and lastGlobalSequence(). Contract tests updated and extended to cover
global-sequence and cross-session subscribeAll invariants.
Every event-derived ServerMessage gains sequence (global) and sessionSequence
(per-session) cursor fields. SessionSnapshot adds lastSequence and
lastSessionSequence at projection-read time. SnapshotComplete data object
terminates the snapshot phase and is registered in the polymorphic module.
replaySnapshot() now emits SnapshotComplete after all SessionSnapshot messages.
SessionStarted audit deferred with TODO comment. DomainEventMapper propagates
both cursors to all mapped messages including InferenceCompleted.
- fixed the tool overlay in tui.
- fixed tool calling - added schemas.
- getting all sessionIds via event store.
- instead of sending all events on the replay - there's a new way for replaying.
- session snapshot is now a thing getting sent for previously created sessions.
- added logging to important parts.
- revert workflowId from WorkflowCompletedEvent.
fixed detekt issues where possible.
fixed disttar failing build because tools is added twice in the server module.
added workflowId where required.
fixed some tests not being recognized because of runBlocking without explicit return type.
formatting + imports.
Implements the full conversational router facade: RouterState, RouterReducer,
RouterProjector, RouterRepository, RouterContextBuilder, RouterFacade, protocol
types, WebSocket wiring, infrastructure factory, and deterministic test suite.
Also fixes spec divergences found in post-implementation review:
- Add SteeringNote domain object to core:context (epic prerequisite)
- Rename RouterFacade.handleChat → onUserInput per spec interface contract
- Add in-memory ConcurrentHashMap conversation history to DefaultRouterFacade
- Make RouterRepository.getRouterState suspend
- Rename RouterConfig.keepLast → conversationKeepLast, fix defaults (6, 4096)
- Refactor InfrastructureModule.createRouterFacade to self-assemble internally
- Fix FileReadTool: allowedPaths was dead constructor param (@SuppressUnusedParameter);
now stored as private val and enforced in validateRequest
- Disable koverVerify on modules tested via testing/ submodules or with
hardware/integration dependencies (24 modules); build gate now passes clean
Lifts inputMode/inputBuffer out of InputState into top-level TuiState,
renames InputMode variants to ROUTER/NAVIGATE/FILTER/STEER, adds
TuiToolRecord and TuiEventEntry for per-session tool/event history,
expands SessionsReducer to track tool lifecycle and recent events, and
adds ToggleApprovalOverlay/ToggleEventOverlay/EnterSteer actions.
Deletes 21 Module.kt scaffolding objects that were never wired into any DI
registry. Removes unused imports across 8 production and 3 test files.
Restores StageExecutor, CyclePolicyResolver, PolicyValidation, and
ReplayContractTest — these are deferred features, not dead code.
Removes 6 files audited as fully unused: StageExecutor interface (shadowed
by kernel), CyclePolicyResolver and PolicyValidation (deferred cycle policy),
ApprovalRoutes (never registered), TuiNavigation (superseded by reducer),
and ReplayContractTest abstract class (no concrete subclasses).
ArtifactRelationshipType and CyclePolicy were audited but retained —
both are actively referenced by production code.
- SessionEventBridge: replaySnapshot() sends all historical events on connect;
streamLive(sessionId) subscribes to live event flow per session
- DomainEventMapper: maps 14 domain event types to ServerMessage; reads
InferenceCompleted response text from ArtifactStore with empty-string fallback
- GlobalStreamHandler: wires bridge on connect for replay, launches per-session
live-stream job on StartSession, cancels all jobs on disconnect; removes
ProtocolError("ping") heartbeat hack
- SessionStreamHandler: fixes replay to use DomainEventMapper instead of
emitting SessionStarted for every event; injects real artifactStore;
removes ProtocolError("ping") heartbeat; adds structured logging throughout
- Main: startup log block showing port, model config, sandbox, stores, tools,
providers
- SessionsReducer: StageCompleted/StageFailed now clears currentStage; adds
5 missing tests (InferenceCompleted, StageStarted, ToolCompleted, SessionPaused)
- RouterPanel: new TUI widget rendering active session's lastResponseText
- TuiWsClient: remove println that was polluting the TUI on WS connect failure
New core:artifacts-store interface + infrastructure/artifacts-cas
implementation: segment files + SQLite index, Blake3 hashing,
group-commit fsync via flushBefore, recovery tail-scan, manual
compactor, and oldest-first disk-cap evictor.
Inference events now carry promptArtifactId / responseArtifactId;
orchestrators put bytes before emitting. SqliteEventStore wraps
its txn in artifactStore.flushBefore so segment data is fsynced
before the event commit, making the crash window non-corrupting
(TailScanner re-indexes orphan tail records on reopen).
Compactor and evictor are mutually exclusive via maintenanceMutex.
Step 9 (cloud sync) deferred to a later epic.
See docs/reviews/2026-05-18-cas-steps-1-8-review.md for the final
review.
Add log4j2/SLF4J deps and log4j2.xml, install Ktor CallLogging, replace
manual System.err.println sites with leveled loggers, and wrap the
EventStore with a LoggingEventStore decorator so every append/appendAll
is logged in one place.