From edeef19ff0c13d11e1b504d46db4aa209286fb3c Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:42:09 +0400 Subject: [PATCH] config: a workstation block, dropped when it names no address (V-485) Health defaults to the supervisor's /health rather than llama-server's, because mavgpud is what answers 503 while the card is held. --- internal/config/config.go | 61 ++++++++++++++++++++++++++++++++++ internal/config/config_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index a95bf77..b152b14 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -224,6 +224,11 @@ type Config struct { // See SearchConfig. Search *SearchConfig `json:"search,omitempty"` + // Workstation — the big model on the owner's desktop, preferred over the + // resident one when its GPU is free. nil / absent / url empty ⇒ homesrv + // behaves exactly as it does today. See WorkstationConfig. + Workstation *WorkstationConfig `json:"workstation,omitempty"` + // Praxis — the ecosystem attention-state service. When configured, maven // calls the Praxis HTTP tools API for attention listing and item lifecycle. // Maven never touches Praxis's database directly (ecosystem invariant: no @@ -1105,6 +1110,44 @@ const ( DefaultKiwixSnippetRunes = 1500 ) +// WorkstationConfig — the big model on the owner's desktop (bugmachine, a +// 7900 GRE with 16GB), fronted by mavgpud. +// +// homesrv cannot grow a GPU, so the resident Qwen3-1.7B is the floor and this +// is the preferred model above it (owner's call, 2026-08-02, docs/offload.md). +// The workstation is never assumed up: its card is often held by a CPT run and +// the machine sleeps. No block, or an empty URL, and homesrv behaves exactly as +// it does today. +// +// Only the prompt crosses the LAN, and the workstation is not "the box". The +// rules in CLAUDE.md about what may leave still apply. +type WorkstationConfig struct { + // URL — where mavgpud listens, e.g. "http://192.168.1.105:8080". Empty ⇒ + // the whole block is normalised to nil and nothing probes anything. + URL string `json:"url,omitempty"` + + // Health — the admission endpoint. Empty ⇒ URL + "/health", which is what + // mavgpud serves. It answers 503 while the card is held, and that is the + // signal, so it must be the supervisor's endpoint and not llama-server's. + Health string `json:"health,omitempty"` + + // Probe — how often admission is re-checked. 0 ⇒ DefaultWorkstationProbe. + // Nothing on the hot path waits for it: the answer is cached and read + // atomically, so this only sets how late Maven notices the card came back. + Probe Duration `json:"probe,omitempty"` + + // Timeout — the per-request budget for a completion on the workstation. + // 0 ⇒ DefaultWorkstationTimeout. A big model on a LAN host is slower than + // the resident one, and a request that overruns falls back to the floor. + Timeout Duration `json:"timeout,omitempty"` +} + +// Workstation defaults, applied in Normalise. +const ( + DefaultWorkstationProbe = 15 * time.Second + DefaultWorkstationTimeout = 90 * time.Second +) + // SearchConfig — the self-hosted SearXNG instance she searches with. // // External search is allowed and off unless configured (CLAUDE.md). Configuring @@ -1500,6 +1543,24 @@ func (c *Config) applyDefaults() { } } + // No address, no preferred model. An unconfigured workstation is the + // default deploy and must be indistinguishable from today. + if c.Workstation != nil && strings.TrimSpace(c.Workstation.URL) == "" { + c.Workstation = nil + } + if c.Workstation != nil { + w := c.Workstation + if strings.TrimSpace(w.Health) == "" { + w.Health = strings.TrimRight(w.URL, "/") + "/health" + } + if w.Probe <= 0 { + w.Probe = Duration(DefaultWorkstationProbe) + } + if w.Timeout <= 0 { + w.Timeout = Duration(DefaultWorkstationTimeout) + } + } + if c.Voice != nil { if c.Voice.RouterThreshold <= 0 { c.Voice.RouterThreshold = DefaultRouterThreshold diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fe275dd..2e315d3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -413,3 +413,56 @@ func TestNormaliseFillsKiwixDefaults(t *testing.T) { t.Error("rewrite: false was not honoured") } } + +// A workstation with no address is not a workstation. The unconfigured deploy +// must be indistinguishable from today, so the block is dropped rather than +// left to fail one probe at a time. +func TestNormaliseDropsAddresslessWorkstation(t *testing.T) { + for _, tc := range []struct { + name string + in *WorkstationConfig + }{ + {"no url", &WorkstationConfig{Probe: Duration(time.Second)}}, + {"blank url", &WorkstationConfig{URL: " "}}, + } { + t.Run(tc.name, func(t *testing.T) { + c := &Config{Workstation: tc.in} + c.applyDefaults() + if c.Workstation != nil { + t.Errorf("kept an unusable workstation block: %+v", c.Workstation) + } + }) + } +} + +// The health endpoint defaults to the supervisor's, not llama-server's: mavgpud +// answers 503 while the card is held, and that refusal is the whole signal. +func TestNormaliseFillsWorkstationDefaults(t *testing.T) { + c := &Config{Workstation: &WorkstationConfig{URL: "http://192.168.1.105:8080/"}} + c.applyDefaults() + if c.Workstation == nil { + t.Fatal("dropped a usable workstation block") + } + if got, want := c.Workstation.Health, "http://192.168.1.105:8080/health"; got != want { + t.Errorf("Health = %q, want %q", got, want) + } + if time.Duration(c.Workstation.Probe) != DefaultWorkstationProbe { + t.Errorf("Probe = %s, want %s", time.Duration(c.Workstation.Probe), DefaultWorkstationProbe) + } + if time.Duration(c.Workstation.Timeout) != DefaultWorkstationTimeout { + t.Errorf("Timeout = %s, want %s", time.Duration(c.Workstation.Timeout), DefaultWorkstationTimeout) + } +} + +// An explicit health URL is left alone: the supervisor may sit behind something +// that does not put /health at the root. +func TestNormaliseKeepsExplicitWorkstationHealth(t *testing.T) { + c := &Config{Workstation: &WorkstationConfig{ + URL: "http://192.168.1.105:8080", + Health: "http://192.168.1.105:9000/ready", + }} + c.applyDefaults() + if got, want := c.Workstation.Health, "http://192.168.1.105:9000/ready"; got != want { + t.Errorf("Health = %q, want %q", got, want) + } +}