Adds ProviderConfig + providers list and per-tool enabled flags to
CorrexConfig, and rewires apps/server/Main to build the provider list
from config instead of the hardcoded buildLlamaProvider() factory. Env
vars (CORREX_MODEL_ID, CORREX_MODEL_PATH, CORREX_LLAMA_URL) remain a
fallback only when no providers are declared in config.
Completes slice A of the config layer alongside commit 0834c70 which
already shipped the TOML parser upgrade and sample config — those
parser changes referenced these types but were committed prematurely
in isolation; this commit makes HEAD buildable.
- Add support for nested table headers ([tools.shell], [tools.file_read], etc.)
- Add support for inline tables (capabilities = { General = 1.0, Coding = 0.7 })
- Add support for JSON-style arrays (shell_allowed_executables = ["bash", "sh"])
- Maintain backward compatibility with old CSV formats (deprecated, logged as warnings)
- Refactor buildConfig() to handle new value types (Map, List, Boolean, Number)
- Update docs/sample-config.toml to use clean TOML shapes
- Add comprehensive tests for inline tables, nested sections, JSON arrays, and fallback parsing
- Remove ugly workarounds: capabilities no longer a CSV string, tool flags now in proper nested sections
Introduces Embedder contract in core:inference with a NoopEmbedder
default (zero vector) and wires both Embedder and L3MemoryStore into
DefaultRouterFacade. Each ChatTurnEvent emission now embeds the turn
content and writes a corresponding L3MemoryEntry, so cross-session
memory is captured at the source of truth (the event emission point).
InfrastructureModule defaults to NoopEmbedder + InMemoryL3MemoryStore
so the system runs without an external embedding model or vector store
wired. The TurboVec adapter stays unwired for now; switching to it is
a configuration concern (Epic 12 config layer).
Also gitignores apps/tui/logs/ (Kotlin TUI runtime logs, pre-Go rewrite).
Adds ChatTurnEvent (role USER|ROUTER) so router conversation history
survives restart and is rebuilt from the event log via projection,
restoring Hard Invariant #1 for the CHAT path. Both CHAT and STEERING
now emit user + router turn events; STEERING additionally keeps its
existing SteeringNoteAddedEvent so the structured directive record is
preserved alongside the conversation turns.
RouterFacade drops the in-memory ConcurrentHashMap of histories and
rebuilds state through the repository after each emission. Reducer
appends turns to conversationHistory. ChatTurnEvent is registered in
eventModule.
Defines the L3MemoryStore contract in core:router (entry, query, hit,
in-memory test default) and a new infrastructure:router:turbovec module
that adapts turbovec via a Python sidecar over stdin/stdout JSON-Lines.
Adapter manages the subprocess via ProcessBuilder with lazy start,
mutex-serialized requests, and shutdown via withTimeout. Wire-format
metadata that turbovec doesn't store (sessionId, turnId, text) is kept
in an in-memory map for now — durable persistence ships with the
upcoming ChatTurnEvent.
Not yet wired into RouterFacade, InfrastructureModule, or DI. Embedding
generation source is a separate concern.
Extract message-rendering logic from LlamaCppInferenceProvider into a
provider-agnostic PromptRenderer in core:inference. SessionOrchestrator
now serializes the rendered ChatMessage list as JSON for the prompt
artifact, so the audit trail reflects what the LLM actually saw.
Resolves the TODO at SessionOrchestrator.kt:691.
- Add STAGE_COMPLETE_TOOL constant and meta-tool handler in executeStage
loop: when LLM calls stage_complete, break out of tool dispatch loop
and proceed to normal validation/transition
- Inject stage_complete tool definition in every inference request so
the LLM always has the option to signal stage completion
- Replace expand-all diff with scrollable diff window in approval
surface (ctrl+x toggles, up/down scrolls, shows line range)
P4-1: Wire steering to actually work (advisory-only):
- RouterFacade emits SteeringNoteAddedEvent via event store in STEERING mode
- SessionOrchestrator reads steering notes from event store and includes them
as ContextEntry objects (sourceType=steeringNote) in stage context packs
- Delete dead SteeringNote data class; single vocabulary = SteeringNoteAddedEvent
- Add optional validateSteering callback to DefaultRouterFacade for future
ValidationPipeline wiring (avoids cross-core dependency)
- Update RouterFacadeTest to assert event emission
P4-2: Router FACE — move hardcoded generation config:
- Add GenerationConfig field to RouterConfig with same defaults
- RouterFacade uses config.generationConfig instead of inline literals
- (Remaining P4-2 items: conversation persistence and coordination
semantics are design decisions requiring ADRs, deferred)
P3-1: Delete 17 empty Gradle modules (core:agents/observability/policies/stages,
infrastructure:scheduler/security/telemetry, all 7 plugins/*, all 3
interfaces/*). Zero source files, zero dependents, identical build.gradle
stubs. Remove from settings.gradle. Keep .adoc spec docs as roadmap refs.
P3-2: Fix docs drift — rename :core:orchestration to :core:kernel in
core-orchestration-submodule-spec.md, rewrite stale chat-transcript
modules-and-spec.md as proper doc.
(CLAUDE.md Router context isolation clarification is local-only,
file is gitignored.)
P3-3: Reduce detekt maxIssues from 999999 to 120 (actual current count ~107).
(CLAUDE.md detekt description updated locally; file is gitignored.)
P3-4: Remove stale locked worktree agent-a98d45277ce4b0040 (contained only
cosmetic test style changes and an unused kotlinx-datetime dep).
P2-1: Guard SessionId NPE with safe-call in SessionsReducer
P2-2: Remove 16 dead event classes + reducer branches; replace with live orchestration events in tests
P2-3: Standardize ChatInput divergences — guard CancelSession, single-arg ProtocolError
P2-4: Remove dead TuiToolRecord.diff and ToolDisplayStatus.REQUESTED
P2-5: Remove dead streamLive from SessionEventBridge
P2-6: Extract SessionsReducerContext data class for 6+ param method
P2-7: Replace StageId("none") sentinel with TypeId.NONE
P2-9: Add TypeId.random() factory on all type-alias IDs
P2-10: Add KDoc on schemaVersion documenting reserved-for-migration
P2-8: Verified RouterReducer string-template already correct (TypeId value class toString)
P1-1: Replace check-then-act with computeIfAbsent for activeSessionJobs to
prevent double-launch. Register pendingApprovals[requestId] BEFORE emitting
ApprovalRequestedEvent to close the approve-before-register window.
P1-2: ToolCompleted with a diff now appends msg.diff to routerMessages
instead of the static "--- diff from $toolName ---" marker.
P1-3: Change routerMessages from List<String> to Map<String, List<String>>
keyed by sessionId.value. RootReducer appends to the correct session's list;
RouterPanel renders only routerMessages[selectedId].
P1-4: On IDLE chat submit, SessionsReducer creates an optimistic
SessionSummary(status="STARTING") and sets selectedId.
processSessionStartedMessage removes any STARTING session and inserts the
real one on echo. RootReducer sets sessionEntered=true for the optimistic
session.
Tests: LaunchRegistrationRaceTest, ApprovalRegisterBeforeEmitTest,
RootReducerTest (diff/per-session/isolation/optimistic-submit),
SessionsReducerTest (optimistic/reconciliation). All P0/P1 tests pass.
./gradlew check green.
Event log integrity (sole source of truth):
- SqliteEventStore: serialize append/appendAll under a Mutex, enable WAL +
busy_timeout + synchronous=NORMAL, roll back on any Throwable. Replaces the
app-computed `SELECT MAX(seq)+1` read-modify-write that dropped events under
concurrent appends (race was invisible to CI: only the in-memory store and
single-threaded :memory: tests were ever exercised).
- Serialization: encodeDefaults + ignoreUnknownKeys, explicit @SerialName on all
46 EventPayload subclasses and event-referenced enum constants, @Serializable on
RiskLevel/RiskAction. Guards the append-only log against silent corruption from
future class renames, field removals, or default-value changes.
Approval/grant security (Invariant: approvals cannot widen authority):
- Tier authorization is now a ceiling (request.tier.level <= max granted) instead
of set membership; SESSION grants bind to a specific toolName instead of matching
everything; handleCreateGrant rejects empty/over-tier/scopeless grants.
Router chat wiring (Phase 0-2): ChatInput round-trip, StartChatSession from IDLE,
router-panel layout, workflows behind Ctrl+W.
Tests: SqliteEventStore concurrency, serialization round-trip + discriminator
stability + unknown-key tolerance, adversarial grant scope/tier; updated existing
approval and reducer tests for the new grant semantics and StartChatSession effect.
- Fix duplicate inference requests after approval resume by registering orchestrator
jobs in activeSessionJobs (ServerModule.launchSessionRun), so the guard in the
OrchestrationResumedEvent subscription prevents spurious duplicate resume.
- Replace blocking Files.readString in computeToolPreview with withContext(Dispatchers.IO)
by making the function suspend — no blocking I/O on the coroutine dispatcher.
- Guard orderedIds.add() in rebuildTools against duplicate invocation IDs on replayed
ToolInvocationRequestedEvent to prevent duplicate tool records in snapshots.
- Add unified-diff preview for file_write tools: computeToolPreview reads existing
file and generates ---/+++ diff before emitting ApprovalRequestedEvent.
- Render diff preview with color coding in ApprovalSurface (@@ yellow, +/- green/red).
- Include current-stage tool records in SessionSnapshot via rebuildTools event replay.
- Fix RouterContextBuilderTest: recursive buildPack helper and 2 tests using class-level
builder instead of their local builder instances (conversationKeepLast mismatch).
- Add suspend keyword to RouterFacadeTest mock, fix buildPack recursion.
- GlobalStreamHandler.handleCreateGrant validates scope (SESSION/STAGE)
and sends ProtocolError to client on validation failure instead of
silent log.warn + drop.
- Derive event stageId directly from GrantScope type, removing
redundant stageIdForEvent tuple.
- SessionOrchestrator integrates ApprovalEngine to check active grants
before requesting user approval for T2+ tool calls.
- ContextEntry gains EntryRole (SYSTEM/USER/ASSISTANT/TOOL) field for
proper chat message role mapping.
- RouterContextBuilder.build is now suspend; uses Tokenizer for
accurate token estimation with fallback to character-based estimate.
- LlamaCppInferenceProvider maps EntryRole to ChatMessage role instead
of heuristic layer-based inference.
Three coordinated changes:
1. DefaultOrchestrationReducer now handles TransitionExecutedEvent,
updating currentStageId so the projected state always reflects the
actual current stage rather than staying stuck at the start stage.
2. DefaultSessionOrchestrator.resume() re-enters the execution loop at
the current stage without re-emitting WorkflowStartedEvent. It reads
currentStageId from the projected state, creates an ExecutionContext
there, and calls enterStage → step exactly as run() does after its
first stage.
3. ServerModule.start() calls resumeAbandonedSessions(), which scans all
RUNNING/PAUSED sessions on startup and launches resume() for each one
whose orchestrator is no longer in memory. Sessions that have a live
deferred are unaffected; only sessions with no coroutine (e.g. the
server was restarted while a tool was executing or approval was pending)
are picked up and re-executed from their current stage.
When a TUI client connects at the exact moment between an
OrchestrationPausedEvent being stored and its corresponding
ApprovalRequestedEvent being stored, the approval projector sees empty
pendingApprovalRequests while orchState.pendingApproval=true. The
stuck-session fix would then append a spurious OrchestrationResumedEvent,
clearing the TUI's pendingApproval display and hiding the approval dialog.
Fix: count OrchestrationPausedEvents vs ApprovalRequestedEvents in the
event log. If there is an unpaired pause (more pauses than requests),
the session just entered the gate — skip the fix entirely.
Also remove spurious ToolExecutionCompleted/Failed emissions from the
kernel orchestrator; those events are emitted by SandboxedToolExecutor
in the infrastructure layer. Keep ToolExecutionRejectedEvent which the
orchestrator does own (rejection happens before executor.execute is
called).
ToolInvocationRequestedEvent was emitted but the corresponding outcome
events were never emitted, so the TUI permanently showed tools as
RUNNING. Now the orchestrator closes every tool invocation with the
appropriate event:
- ToolExecutionCompletedEvent (Success path)
- ToolExecutionFailedEvent (Failure path)
- ToolExecutionRejectedEvent (approval denied path)
Sessions that had an approval resolved (or never needed one) but never
received OrchestrationResumedEvent would show the last tool as RUNNING
forever with no subsequent events. Three coordinated fixes:
1. SessionOrchestrator now emits OrchestrationResumedEvent in both the
approved and rejected approval branches, so the paused flag clears
correctly going forward.
2. SessionEventBridge.replaySnapshot() detects the stuck pattern
(pendingApproval=true with no unresolved requests) and appends a
synthetic OrchestrationResumedEvent so historical sessions self-heal
on next TUI connect.
3. DomainEventMapper maps OrchestrationResumedEvent → SessionResumed;
TUI SessionsReducer/SnapshotPhaseReducer handle the new message,
clearing pendingApproval and setting status back to ACTIVE.
Also adds server-restart recovery path in DefaultSessionOrchestrator
(emit decision + resume events directly when no live deferred exists)
and pre-registers pending approvals in ServerModule.start() to close
the race between reconnect and ApprovalResponse routing.
Move ProcessResult artifact content storage into dispatchToolCalls so it
fires for all outcomes (success, ERROR, FATAL) — the spec mandates the
artifact MUST always be produced.
- dispatchToolCalls now stores ProcessResult JSON in CAS + cache for
every tool call result, using the current toolCall's function.arguments
as the command field (not firstOrNull)
- Replace storeFailedProcessResult with emitProcessResultEvents — event
emission only, content already stored by dispatchToolCalls
- FATAL and ERROR branches both now emit artifact lifecycle events,
fixing the gap where retryable ToolResult.Failure produced no artifact
- Add field to ProcessResultArtifact ("success"/"failed")
- Materialize ToolResult.Failure as a failed ProcessResult artifact
in the orchestrator, with artifact lifecycle events emitted
- Non-recoverable ToolResult.Failure now fails the stage ("FATAL:")
instead of being silently fed back as LLM context
- Fix enterStage to return Continue on non-retryable failures,
enabling back-edge transitions (fix/retry loops)
- Add ArtifactFieldEquals transition condition with flat and
nested dot-notation field access + FieldOperator enum
(eq, neq, gt, gte, lt, lte)
- Wire artifact_field_equals through ConditionFactory and
TomlWorkflowLoader (condition_field, condition_operator)
- Cache ProcessResult artifact content for evaluation context
- Add 16 tests covering all condition operators and error cases
- DefaultTransitionResolver catches condition evaluation errors
and returns Blocked instead of crashing
- 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)
SessionOrchestratorIntegrationTest: wire approvalRepository into
OrchestratorRepositories constructor which gained the field in the kernel
refactor. TambouiKeyMapperTest: correct ctrl-h to alt-h — the mapper has
always placed ToggleApprovalOverlay under the Alt branch, not Ctrl.
SessionOrchestrator.emit: replace substring(0,7) with take(7) to avoid
StringIndexOutOfBoundsException when session IDs are shorter than 7 chars.
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
Two issues causing "no transition condition matched":
1. emitToolArtifacts only emitted ArtifactCreatedEvent; the artifact_validated
transition condition requires VALIDATED phase, which needs the full lifecycle
(Created → Validating → Validated). Tool success is sufficient grounds for
auto-validation, so emit all three events.
2. DefaultSessionOrchestrator was building stageArtifacts from LiveArtifactRepository
which uses a hot async subscription — cache was always empty at lookup time.
Now reads ArtifactValidatedEvents directly from the synchronous EventStore.read().
LiveArtifactRepository uses a hot subscription that does not replay history,
so its cache was always empty when verifyProduces ran immediately after emit.
Fix by reading directly from EventStore (synchronous) instead of the repository.
Also emit ArtifactCreatedEvent for non-llmEmitted produces slots after stage
validation passes, and fix tool parameter decoding to use JsonPrimitive.content
so escape sequences like \n are resolved rather than passed as literal backslash-n.
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.
- SessionOrchestrator: wire T2/T3/T4 approval gate before tool execution;
emit OrchestrationPausedEvent/ApprovalRequestedEvent, await decision,
return rejection as ERROR context entry so LLM sees the denial;
propagate tool ERROR entries as StageExecutionResult.Failure
- SandboxedToolExecutor: remove dead code and simplify
- InfrastructureModule: minor wiring cleanup
- LlamaCppInferenceProvider / build.gradle: related build fixes
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.