You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
HTTP responses from probed servers are now read with a size cap. The launcher discovers servers by address, so a hostile process squatting on a configured port is an in-scope adversary — and health checks, model listing, and llama-server's /props consumed the response body with no size limit, bounded only by the 2-second client timeout. Since discovery probes several addresses in parallel (on every menu refresh tick, too), one squatter streaming data over loopback could force large concurrent allocations in the one-shot CLI or the long-lived MCP adapter. Every backend body read now goes through io.LimitReader: 8 KiB for status-like payloads (health and backend-discrimination probes, error-message bodies, drained load/unload responses) and 1 MiB for structured payloads that scale with server state (model lists, /props with its embedded chat template). A body past the cap arrives truncated and simply fails the parse that follows, so an oversized health or model-list response is rejected instead of consumed.
The MCP adapter's HTTP listener now enforces connection timeouts, and tool output is capped at 1 MiB per stream. The http.Server previously set no timeouts, so a stuck or hostile client could hold connections open indefinitely; it now sets ReadHeaderTimeout (10 s), IdleTimeout (2 min), and WriteTimeout (10 min — generous because load_profile legitimately waits up to 5 minutes for a model load). Separately, the adapter accumulated the CLI's stdout/stderr into unbounded buffers, so a runaway subprocess (e.g. a logs invocation that never stops producing output) could grow the adapter's memory and the MCP response without limit; each captured stream is now capped at 1 MiB, with content past the cap dropped and a [output truncated: 1MiB cap reached] notice appended so the caller knows the output is incomplete.
The MCP adapter now validates tool-supplied target/profile arguments before forwarding them to the CLI.tail_log, stop_server, and unload_model passed their optional argument to the CLI as a raw positional, so a hostile (e.g. prompt-injected) client could smuggle a flag or a subcommand keyword through it: tail_log with target: "clean" executed the destructive logs clean through the read surface — available even under --read-only — and target: "-f" made the adapter block forever on logs --follow. All three tools now pass their positional through a single validation gate that rejects values starting with - or matching a launcher subcommand keyword (clean, load, stop, …), returning an MCP tool error without invoking the CLI. Legitimate backend names and host:port targets are forwarded unchanged; the CLI's own logs/stop/unload parsing is untouched (ADR-0008).
Changed
Discovery no longer issues an extra GET /props request per llamacpp instance.DiscoverRunningInstances populated a RunningInstance.ResolvedParams field that nothing ever read — at the cost of a live /props query against every reachable llama-server on every discovery pass, including each menu refresh tick. The write-only field and its probe are removed, so discovery now makes one fewer round trip per llamacpp instance; the load-time drift check (ADR-0007) keeps its own /props query, so nothing that a user sees changes. Three unused internal helpers (IsServerAlive, FormatMemoryLine, percentString) were deleted in the same pass.
Fixed
A managed llama-server that dies during startup is now detected instead of reported as running.startManagedServer forked the child but never reaped it, so a fast-exiting server (port already in use, bad args, missing model) became a zombie — and a zombie still satisfies kill(pid, 0), so the ~500 ms liveness check reported the dead process as alive and StartServer returned a RunningInstance for a server that had already exited (the real failure only surfaced later as a health-check timeout). The child is now reaped by a cmd.Wait goroutine whose result feeds the grace-period check: if the process exits within the startup window the launcher returns the "server exited immediately after start" error with the log tail, and if it stays up the goroutine stays parked to reap it whenever it does exit, so no zombie is left behind.
llama-launcher list now aligns the ★ favourite marker by display width. The list command measured its column widths with byte length (len) while the interactive menu uses visible (rune) width, so a profile description or title containing a multi-byte character — the em dash in the shipped example, for one — pushed the ★ column out of alignment relative to the ASCII rows. The column widths (and the marker's right-alignment padding) are now measured with the same visibleWidth helper the menu uses, so the marker lines up regardless of multi-byte content.
Closing a pop-up now restores the terminal cursor.showPopup hid the cursor while displaying a pop-up but never re-showed it, so with the default auto_close: true, opening any pop-up (e.g. "Show model config" or an error) and pressing a key exited the process with the cursor left invisible until the next reset. The popup-exit path now emits the show-cursor escape after the keypress, mirroring the interactive menu's cursor restoration.
Auto-stop / auto-unload now identifies the target instance by address and backend. When activating a profile, LoadProfile skips the instance it is about to (re)use so it does not tear that server down — but it matched only on host:port, so a different backend squatting on the profile's address (e.g. a leftover llama-server on :8080 while activating an ollama profile bound there) was mistaken for the target and skipped. With auto_stop_server: true the launcher then stopped every other instance but left the actual blocker running, and the subsequent start failed to bind the port. Both the auto-stop and auto-unload loops now treat an instance as the target only when its address and backend both match; a same-address instance of a different backend is a blocker and is stopped/handled (ADR-0004, ADR-0006).
The interactive menu now treats the instance with a model loaded as its "loaded" instance. When several servers were running, the menu picked the first discovered instance as its primary regardless of whether that one had a model — so with an idle LM Studio (which sorts first) alongside an Ollama instance actually serving a model, the menu surfaced the idle server's log and config and rendered an empty Model: line in simple mode. Selection now returns the first discovered instance that actually has a model loaded, falling back to the first instance only when none has one.
Stopping an Ollama instance no longer runs a broken CLI call or a host-wide process sweep.Ollama.TryStop shelled out to ollama stop with no model argument — which the ollama CLI rejects, so the hook errored on every call — and then fell back to pgrep -f "ollama serve" and SIGTERMing every matching PID, i.e. every Ollama instance on the host regardless of the target address (that fallback survived only because the errored return was swallowed). TryStop is now a no-op: the launcher already stops the specific instance by signalling whatever process is listening at the target host:port (the address-scoped lsof/PID path in EnsureStopped), which is per-instance and version-independent. It deliberately does not use ollama stop <model> (that only unloads a model from a still-running ollama serve, so it would not free the listener, and the subcommand is absent on older ollama versions). Separately, EnsureStopped now surfaces a backend stop-hook error to stderr instead of discarding it, while still treating the hook as best-effort and non-blocking, matching the documented behaviour (TDD §6.5).
status --json now reports every running instance, not one per backend. With auto_stop_server: false two instances of the same backend can run on different ports, and human status lists both — but the JSON path (and therefore the MCP server_status tool) took the first discovered instance per enabled backend and silently dropped the rest. The array now carries one entry per discovered running instance (each with its own address), plus a running: false entry for every enabled backend with no running instance. The per-entry field set and the exit-code semantics (0 if anything is running, else 1) are unchanged.
LM Studio profiles now actually apply batch_size and flash_attn, and no longer claim an unsupported gpu_layers mapping. The load call sent only model and context_length, while the config's parameter table and the "Show model config" pop-up claimed gpu_layers (99→"max", 0→"off"), batch_size (→eval_batch_size), and flash_attn were in effect — so e.g. gpu_layers: 0 on an lmstudio profile still loaded with LM Studio's GPU default. batch_size and flash_attn are now forwarded to POST /api/v1/models/load under LM Studio's REST field names (eval_batch_size, flash_attention). gpu_layers could not be implemented: LM Studio's REST load endpoint accepts no GPU-offload field (its only GPU knob, offload_kv_cache_to_gpu, controls KV-cache placement, not layer offload — GPU offload ratios exist only in the lms CLI and SDKs). The parameter table and the pop-up therefore no longer present gpu_layers as lmstudio-applicable; configure GPU offload in LM Studio itself.
Configured sampling parameters now actually reach llama-server. The five sampling settings — temperature, repeat_penalty, top_k, top_p, min_p — are documented as llamacpp-applicable and shipped in the example config's defaults:, but the llamacpp argument builder never emitted them, so llama-server always ran with its own built-in sampling defaults and user-configured values were silently ignored. They are now passed as --temp, --repeat-penalty, --top-k, --top-p, and --min-p when set, placed before extra_args so an explicit override there still wins (llama-server uses the last occurrence of a repeated flag). This also removes the residual drift notice on re-load for profiles whose configured sampling values differ from llama-server's built-in defaults: the live values reported by /props now match what was configured.
Re-running load on an already-active llamacpp profile no longer reports false parameter drift. The idempotent no-op path (ADR-0007) diffs the freshly resolved profile against the live parameters from llama-server's /props, but /props reports only a subset of the launch parameters (context_size, parallel, and the sampling settings) — the fields it never reports (gpu_layers, threads, batch_size, flash_attn, …) came back nil and were counted as drift. With the shipped defaults setting those fields, the second load of any llamacpp profile printed a bogus "parameters have drifted" notice that --restart could never clear. The drift comparison now skips any field that either side does not carry a value for (unknown ≠ drifted); a genuinely changed shared field (e.g. context_size) is still reported.
Corrected stale documentation that contradicted the no-persisted-state design.CONTEXT.md claimed the launcher kept a backend JSON field on a persisted ServerState schema and that each server's active Profile/Model was "recorded in its per-server state file" — but no state file or ServerState type exists: instance identity and the active Profile/Model are derived live by probing each server at its host:port (ADR-0006, ADR-0007), and any legacy state-*.json files are best-effort deleted on startup. The "flagged ambiguities" note now describes the surviving RunningInstance.Backend field instead. Separately, llama-launcher.TDD.md §14 pointed contributors at an in-repo skills/coding-standards/SKILL.md that does not exist (the repo's skills/ holds only manage-llm-server); it now references the external personal ~/.claude/skills/coding-standards/SKILL.md skill. Docs only, no code change.
Automatic log cleanup no longer deletes the log of a running server. With log_retention set (the shipped default is 7), the cleanup pass that runs before each new log file is created had no access to the loaded config, so it could not discover running instances and deleted the still-open log of any server running longer than the retention window — the server kept writing to the unlinked file and llama-launcher logs reported no managed log. The automatic path now threads the live config through to the cleanup, so logs belonging to running servers are always skipped, exactly as logs clean already did (TDD §9.1).
Fixed a data race on each backend's per-server API key. Every backend struct (LlamaCpp, Ollama, LMStudio) holds an unexported apiKey field that applyAPIKeys writes from the config-load goroutine at the end of LoadConfig/Reload, while DiscoverRunningInstances reads it from parallel probe goroutines via HealthCheck and ListRunningModels — an unsynchronised write/read that go test -race flags (two concurrent LoadConfig calls also raced writer-vs-writer). The field is now guarded by a per-backend sync.RWMutex: setAPIKey takes the write lock and every read goes through a getAPIKey accessor under the read lock, so the same key still reaches the same requests but access is race-free. Behaviour is unchanged; go test -race ./... is now clean.