feat: correx-managed model lifecycle slice 5 — Go TUI model swap + telemetry

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).
This commit is contained in:
2026-06-01 13:36:01 +04:00
parent 7b1df95627
commit 55a18b4b3a
9 changed files with 174 additions and 2 deletions
@@ -283,6 +283,19 @@ sealed interface ServerMessage {
override val sessionSequence: Long? = null,
) : ServerMessage, NonEventMessage
/**
* The configured local models and which one is currently resident. Sent once in the initial
* snapshot when correx manages the model process, so clients can offer a swap picker.
*/
@Serializable
@SerialName("model.list")
data class ModelList(
val models: List<String>,
val current: String?,
override val sequence: Long? = null,
override val sessionSequence: Long? = null,
) : ServerMessage, NonEventMessage
/**
* Live resource gauge pushed periodically on the global stream (not event-derived). All fields
* are nullable: GPU fields are null on a non-NVIDIA host, `processRssMb` is null when no model
@@ -149,6 +149,16 @@ class GlobalStreamHandler(private val module: ServerModule) {
ServerMessage.WorkflowList(workflows = workflows),
)))
// Configured models + current, so the client can offer a swap picker (managed path only).
module.modelSwapper?.let { swapper ->
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(
ServerMessage.ModelList(
models = swapper.availableModelIds(),
current = swapper.currentModelId(),
),
)))
}
// Initial resource gauge so a freshly-connected client renders VRAM/RAM immediately.
val snapshot = withContext(Dispatchers.IO) { module.resourceProbe.probe() }
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(snapshot.toResourceStatus())))