Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
+25 -14
View File
@@ -4,6 +4,7 @@ import (
"context"
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"strconv"
@@ -57,7 +58,8 @@ type modelsPage struct {
// the reply. On its own connection the swap only blocks the swap.
func handleModels(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, swapConn modelController, session *webauthn.PasskeySession, requireStepUp bool) {
if core == nil {
http.Error(w, "models disabled (no -core)", http.StatusServiceUnavailable)
writeProblem(w, r, http.StatusServiceUnavailable, problemCoreUnavailable,
"models disabled (no -core)", nil)
return
}
mc, ok := swapConn, swapConn != nil
@@ -65,7 +67,8 @@ func handleModels(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, swap
mc, ok = core.(modelController)
}
if !ok {
http.Error(w, "models unavailable: core connection does not support model swap", http.StatusServiceUnavailable)
writeProblem(w, r, http.StatusServiceUnavailable, problemModelsUnavailable,
"models unavailable: core connection does not support model swap", nil)
return
}
ctx := r.Context()
@@ -73,12 +76,14 @@ func handleModels(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, swap
if r.Method == http.MethodPost {
if !stepUpOK(session, requireStepUp) {
http.Error(w, "step-up required: assert a passkey first", http.StatusForbidden)
writeProblem(w, r, http.StatusForbidden, problemStepUpRequired,
"step-up required: assert a passkey first", nil)
return
}
path := strings.TrimSpace(r.FormValue("model_path"))
if path == "" {
http.Error(w, "model_path required", http.StatusBadRequest)
writeProblem(w, r, http.StatusBadRequest, problemInvalidRequest,
"model_path required", nil)
return
}
// Only the path comes off the form. n_ctx and n_gpu_layers are load
@@ -93,20 +98,26 @@ func handleModels(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, swap
page.Msg = "loaded " + res.Model + " (" + strconv.FormatInt(res.TookMs, 10) + "ms)"
log.Printf("models: swapped to %s (%s) in %dms", res.ModelPath, res.Model, res.TookMs)
case errors.Is(err, ipc.ErrForbidden):
http.Error(w, "refused: that model is not in phraser.swap_models, or step-up was not asserted", http.StatusForbidden)
writeProblem(w, r, http.StatusForbidden, problemModelsForbidden,
"refused: that model is not in phraser.swap_models, or step-up was not asserted",
fmt.Errorf("swap model %q: %w", path, err))
return
case errors.Is(err, ipc.ErrUnknownMethod):
http.Error(w, "swap not configured on this core", http.StatusServiceUnavailable)
writeProblem(w, r, http.StatusServiceUnavailable, problemModelsUnavailable,
"swap not configured on this core", fmt.Errorf("swap model %q: %w", path, err))
return
case res.NoBackend:
page.Err = "swap failed AND the rollback failed — no model is loaded. She is answering from templates and routing on the classifier. Try loading a model again; a restart is not needed."
log.Printf("models: swap to %s failed and the rollback failed, no model loaded: %v", path, err)
page.Err = inlineProblem(r, problemModelsUnavailable,
"swap failed AND the rollback failed no model is loaded. She is answering from templates and routing on the classifier. Try loading a model again; a restart is not needed.",
fmt.Errorf("swap model %q and rollback: %w", path, err))
case res.RolledBack:
page.Err = "swap failed, rolled back to " + res.Model + " — she is still answering, with the old model"
log.Printf("models: swap to %s failed, rolled back: %v", path, err)
page.Err = inlineProblem(r, problemModelsUnavailable,
"swap failed, rolled back to "+res.Model+" — she is still answering, with the old model",
fmt.Errorf("swap model %q, rolled back to %q: %w", path, res.Model, err))
default:
page.Err = "swap failed: " + err.Error()
log.Printf("models: swap to %s failed: %v", path, err)
page.Err = inlineProblem(r, problemModelsUnavailable,
"swap failed; the current model state is shown below",
fmt.Errorf("swap model %q: %w", path, err))
}
}
@@ -115,8 +126,8 @@ func handleModels(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, swap
if errors.Is(err, ipc.ErrUnknownMethod) {
page.Off = true
} else {
log.Printf("models: status: %v", err)
http.Error(w, "core read failed", http.StatusBadGateway)
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
"core read failed", fmt.Errorf("read model status: %w", err))
return
}
}