- generic SystemResourceProbe: owner-agnostic /proc/meminfo system RAM
overlaid on the vendor GPU probe, wired on both managed and static paths
so the VRAM/GPU/RAM gauge renders with an external llama-server (was dormant)
- F-002: classify provider 4xx (e.g. llama.cpp 400) as terminal, not retryable
(except 408/429); stops retrying deterministic request failures to exhaustion
- F-003: FileSystemPromptLoader expands leading ~ / ~/ to user home
- F-004: map InferenceFailedEvent + RetryAttemptedEvent to new
ServerMessage.InferenceFailed / RetryAttempted, decoded+rendered in tui-go;
ArtifactContentStoredEvent explicitly mapped to null (internal, no warn)
- tests: tilde expansion, SystemResourceProbe (static/managed/fail-soft)
Two operator features over the existing WebSocket protocol, plus a tui-go nav fix.
Config editor (g / palette "config"):
- core:config gains a thin registry stack: OrchestrationKnobs ([orchestration]
block), CorrexConfigWriter (round-trip TOML serializer; parseToml(write(c))==c),
ConfigHolder (AtomicReference live config), and EditableConfig (allowlist of
editable fields + patch applier; security fields are absent so they can never
be patched).
- ConfigService validates -> persists (atomic temp+move) -> swaps the holder ->
rebuilds config-derived services. Live-apply is scoped to safe seams: stage
timeout, compaction threshold (now a supplier), router knobs (routerFacade
rebuild), and personalization/project toggles all take effect on the next
session/turn; in-flight sessions keep the config they started with.
server.host/port are persisted + badged "restart required", not hot-swapped.
- Protocol: GetConfig / UpdateConfig / ConfigSnapshot; TUI overlay with
type-aware editing (bool toggle, enum cycle, inline int/string edit) and save.
Artifact viewer (v / palette "artifacts"):
- Server assembles a session's artifacts from the event log and resolves bytes
from the CAS store (StreamQueries.listArtifacts) — a replay-neutral snapshot
read. TUI overlay lists artifacts and shows scrollable content.
tui-go fix: workflow failure no longer clears selectedID (which yanked the user
to the list); listNav lands on the first session when nothing is selected
instead of wrapping to a random one.
Config is not event-sourced (it lives in TOML); editing stays outside the log.
SessionWorkspaceBoundEvent already recorded the bound workspace root but
it was never surfaced. Map it to a new session.workspace_bound frame
(sealed ServerMessage variant, auto-registered; re-emitted on reconnect
via the snapshot replay), carry it on Session.WorkspaceRoot, and show it
home-abbreviated in the status bar (⌂ ~/Programs/correx). Golden test
pins the decode.
Three QA-found issues in the approval gate:
- displayState gated the in-session/approval surfaces on hasApproval
before sessionEntered, so moving the list cursor onto a session with a
pending gate auto-opened it, and `l` back-to-list couldn't escape
(the gate re-popped). Require sessionEntered first.
- The band ran every preview through the two-column diff renderer, so a
shell tool's argv JSON rendered as an identical-column "diff". Detect
real unified diffs (isUnifiedDiff); render other previews as plain
text, and drop the ^x fullscreen hint when there's nothing to expand.
- The footer duplicated the band's approve/reject/steer/diff keys. It now
shows navigation (l back / e events / q quit) instead.
The plane-2 tool-call assessor records verified preconditions
("[PATH_OUTSIDE_WORKSPACE] …") and the server already ships them in
RiskSummaryDto.rationale, but the Go TUI's RiskSummaryDto had no
Rationale field — so the justification was silently dropped at decode
and the gate showed only an opaque tier.
Decode the rationale, carry it on Approval, and render it under the
header in the approval band (warn-marked, above the diff). This closes
the last deferred item of the plane-2 slice-1 plan: the assessment
surfaced to the approval UX. Golden test pins the rationale decode.
Rebuild the Go TUI transcript from the event stream instead of
optimistic local echoes. ChatTurnEvent now maps to ServerMessage.ChatTurn
(chat.turn); the TUI auto-vivifies sessions and renders user+router turns
from events. SessionStarted is de-special-cased into SessionAnnounced
(still carries workflowId), dropping the router.response shadow and
optimistic echoes.
Delete the unused Kotlin TUI (apps/tui) and add a kotlinx<->Go
golden-fixture test to lock the wire protocol. Gitignore server runtime
logs.
- gate session-list nav on idle so in-session arrows/jk don't move the list (5a)
- approval dismiss on esc + in-session 'a' reopens a pending approval (5b)
- auto-focus a newly started workflow session (was running invisibly)
- approval actions on Ctrl chords (^a/^r/^x) matching the modal; bare/Alt letters no longer approve; diff closes on ^x/esc
- steer hint in the approval modal; session UUID in the input bar
- env-gated debug logging (CORREX_TUI_LOG) with k.Alt in the key trace
Go TUI decodes model.changed / model.list / resource.status and renders a
status-bar VRAM/GPU%/RAM gauge plus a models overlay (m key / palette 'models'):
lists configured models with the resident one marked, enter swaps (SwapModel),
c clears the pin (ClearModelPin). All three new server messages are
non-event-bearing control/gauge frames, applied immediately like
provider.status_changed.
Server addition required for the picker: ServerMessage.ModelList (model.list,
NonEventMessage) sent once in the initial snapshot from
ManagedInferenceRouter.availableModelIds()/currentModelId() — the TUI otherwise
cannot enumerate swap targets.
Completes the 5-slice model-lifecycle feature. ./gradlew check green; go build/vet clean.
Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 5 of 5).