Address Gadfly findings on #90 (blocking round)
Build image / build-and-push (push) Successful in 17s

- Remove config.AgentConfig.Ready() — dead after the refactor (no non-test
  callers), and its doc comment ("route registration and capabilities gate on
  this") was now false. EffectiveAgent.Ready() carries the same logic and IS
  used, see below.
- agentHolder now uses eff.Ready() and eff.APIKey instead of an inline check and
  a redundant apiKey field it held separately. This makes EffectiveAgent.APIKey/
  Ready() production-used rather than test-only, drops the duplicated readiness
  check, and simplifies newAgentHolder's signature.
- settingsPayload no longer swallows an EffectiveAgent error into a misleading
  empty "effective" view (which would read as "nothing configured"). It returns
  the error; the handlers surface it as a 500. EffectiveAgent re-reads the row
  GetInstanceSettings just returned, so a failure there is a real DB fault.
- Frontend streamChat handles 503 (assistant disabled at runtime) distinctly
  from 404 (endpoint absent) — the backend returns 503 AGENT_DISABLED now, which
  the old code mislabelled.
- Fixed the api.go comment that still said a disabled request gets a 404 — it's
  a 503.
- updateSettings uses the shared parseNullable[bool] instead of a bespoke
  parseNullableBool (now removed).
- NewRunner drops its empty-spec guard; agentmodel.Resolve already owns that
  check, so one place decides what a valid spec is.
- rebuild-on-resolve-error now documents WHY it keeps the current Runner rather
  than tearing down a working assistant on a transient DB blip: the state is
  persisted, the next rebuild reconciles, and killing a live assistant on a read
  hiccup is worse than a brief stale window.

Not changed: the store's version-conflict fallback returning GetInstanceSettings'
error instead of ErrVersionConflict when that read also fails. That's the exact
pattern every other version-guarded update uses (gardens/objects/plants); making
only this one differ would be the inconsistency. A DB read failing immediately
after the guarded UPDATE on the same local SQLite file is a disk-fault edge case,
and surfacing that error is defensible.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
2026-07-21 22:17:25 -04:00
co-authored by Claude Opus 4.8
parent 41c28592a2
commit 9ab454373b
6 changed files with 51 additions and 47 deletions
+24 -21
View File
@@ -53,22 +53,26 @@ type effectiveView struct {
AgentLive bool `json:"agentLive"`
}
func (h *handlers) settingsPayload(c *gin.Context, st *domain.InstanceSettings) settingsResponse {
// settingsPayload builds the response, or an error. It does NOT swallow an
// EffectiveAgent failure into a misleading empty "effective" view — an empty
// view would report no model and no key, which reads as "nothing configured"
// rather than "we couldn't read it". Since EffectiveAgent re-reads the same row
// GetInstanceSettings just returned, a failure here is a genuine DB fault worth
// surfacing as a 500, not papering over.
func (h *handlers) settingsPayload(c *gin.Context, st *domain.InstanceSettings) (settingsResponse, error) {
eff, err := h.svc.EffectiveAgent(c.Request.Context())
if err != nil {
// The settings row read succeeded to get here, so this is unexpected; fall
// back to an empty effective view rather than failing the whole response.
eff = service.EffectiveAgent{}
return settingsResponse{}, err
}
return settingsResponse{
Settings: st,
Effective: effectiveView{
Model: eff.Model,
Enabled: eff.Enabled,
HasApiKey: h.cfg.Agent.OllamaCloudAPIKey != "",
HasApiKey: eff.APIKey != "",
AgentLive: h.agent.get() != nil,
},
}
}, nil
}
func (h *handlers) getSettings(c *gin.Context) {
@@ -77,7 +81,12 @@ func (h *handlers) getSettings(c *gin.Context) {
writeServiceError(c, err)
return
}
c.JSON(http.StatusOK, h.settingsPayload(c, st))
payload, err := h.settingsPayload(c, st)
if err != nil {
writeServiceError(c, err)
return
}
c.JSON(http.StatusOK, payload)
}
// settingsUpdateRequest is the PATCH body. agentModel "" means inherit the env
@@ -97,7 +106,9 @@ func (h *handlers) updateSettings(c *gin.Context) {
}
// agentEnabled: absent or null → inherit (nil); true/false → explicit override.
enabled, err := parseNullableBool(req.AgentEnabled)
// The shared parseNullable does exactly this three-way decode; present is
// irrelevant here because absent and null both mean "inherit".
enabled, _, err := parseNullable[bool](req.AgentEnabled)
if err != nil {
writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "agentEnabled must be true, false, or null")
return
@@ -123,18 +134,10 @@ func (h *handlers) updateSettings(c *gin.Context) {
// settings. Mirrors the same reasoning as the history-write detachment.
h.agent.rebuild(context.WithoutCancel(c.Request.Context()))
c.JSON(http.StatusOK, h.settingsPayload(c, st))
}
// parseNullableBool maps a JSON field that may be absent, null, or a bool to a
// *bool: nil for absent/null (inherit), else a pointer to the value.
func parseNullableBool(raw json.RawMessage) (*bool, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
payload, err := h.settingsPayload(c, st)
if err != nil {
writeServiceError(c, err)
return
}
var b bool
if err := json.Unmarshal(raw, &b); err != nil {
return nil, err
}
return &b, nil
c.JSON(http.StatusOK, payload)
}