v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 20:12:56 +02:00
parent 114ff2761a
commit 1453cfc69b
68 changed files with 3953 additions and 116 deletions
+19 -8
View File
@@ -789,7 +789,7 @@ func main() {
}
if backupMgr != nil && cfg.Offsite.Enabled && cfg.Hub.URL != "" && cfg.Hub.APIKey != "" {
bridge := &offsiteapply.Bridge{
Cfg: cfg,
Cfg: cfg,
// Decision 69 (v0.289.0): the box sends its PUBLIC key to the hub's registrar, which pins it
// append-only; the box never receives the sub-account password.
Registrar: offsiteRegistrar,
@@ -1507,13 +1507,18 @@ func main() {
// every driveless app's recovery unit) were invisible, and it never reported a free-byte figure or
// named a drive.
//
// CADENCE: DAILY, at 03:30. A fill is a slow-moving quantity — the thing that fills a disk is a
// customer's photo library or a nightly backup, not a spike — so a shorter interval buys no
// earlier warning and only costs statfs calls. 03:30 is deliberately BEFORE the nightly app-data
// legs (db-dump / tier2 / offbox), so a customer who is about to lose a backup to lack of space
// hears about it while there is still a night's margin, rather than after the failure.
// The interval is NOT a cooldown: repeats are impossible because the check is edge-triggered per
// filesystem, and the hub owns cooldown regardless.
// CADENCE: every fillWatchInterval (10 min), PLUS the daily 03:30 run, PLUS once after startup.
// The original reasoning was "a fill is slow-moving, so daily is enough"; two measurements proved
// it wrong. R-363 (2026-08-21): a filesystem filled to 99% just after a daily run and the watcher
// said nothing while the backup reserve was already refusing apps. R-547 (2026-09-17): a root
// filesystem held at 96% for ten minutes raised no alarm of any kind. A daily sweep cannot carry
// the word BEFORE; an interval no longer than the shortest measured window can. The cost is a
// statfs per watched filesystem and one summary log line per run.
// The interval is NOT a cooldown and adds no repeat mails: the check is edge-triggered against
// PERSISTED bands with a hysteresis gap (fillwatch TestWarnsOnceThenIsSilent,
// TestThresholdsKeepTheirHysteresisGap), and the hub owns cooldown regardless. The 03:30 run is
// kept so the check still lands just before the nightly app-data legs. Pinned by
// TestFillWatchRunsOnAnInterval.
fillWatcher := fillwatch.New(
filepath.Join(cfg.Paths.DataDir, "fillwatch-state.json"), logger,
func() []fillwatch.Target { return fillTargets(cfg, sett) },
@@ -1544,6 +1549,7 @@ func main() {
// space the household can free on the kept-data list. Nothing is deleted by the box (D3).
fillWatcher.SetExtra(func(t fillwatch.Target) string { return keptSpaceSentence(stackMgr, t.Path) })
sched.Daily("fill-watch", "03:30", func(ctx context.Context) error { return fillWatcher.Check() })
sched.Every("fill-watch-interval", fillWatchInterval, func(ctx context.Context) error { return fillWatcher.Check() })
// AND ONCE SHORTLY AFTER STARTUP. A box that BOOTS with a filesystem already over the line must
// warn now, not up to 24 hours later — that is the R-100 shape, a real fault visible only after a
@@ -2437,6 +2443,11 @@ func (s gatedAppStopStarter) StartStack(name string) error {
// as unreadable (and be skipped, §8.4), never as a filesystem worth warning about.
const fillWatchStartupDelay = 90 * time.Second
// fillWatchInterval is how often the fill check runs between the daily sweeps (R-363, R-547). It must
// not exceed the shortest fill window that was measured going unannounced (ten minutes, R-547): any
// window at least this long then contains a check. Pinned by TestFillWatchRunsOnAnInterval.
const fillWatchInterval = 10 * time.Minute
// fillTargets is §8.1's watch list: the app-data volume, the system-data volume, and every
// registered drive. Resolved at CHECK time, not at startup, so a drive added or decommissioned
// between checks is picked up without a controller restart.
@@ -0,0 +1,56 @@
package main
import (
"os"
"regexp"
"strings"
"testing"
)
// R-208: `ARG VERSION` / `ARG GIT_COMMIT` declared ABOVE `RUN go mod download` put a per-build value
// into that RUN's cache key, so every release re-downloaded the modules (measured: 208 cache records,
// every one with usage count 1). The consequence pinned here, in the builder stage: no ARG whose value
// changes per build is declared before the module-download step, and both are still declared before
// the `go build` that reads them (or the binary would report "dev"/"unknown").
func TestR208_DockerfileVersionArgsSitBelowModuleDownload(t *testing.T) {
src, err := os.ReadFile("../../Dockerfile")
if err != nil {
t.Fatal(err)
}
lines := strings.Split(string(src), "\n")
argRe := regexp.MustCompile(`^\s*ARG\s+(VERSION|GIT_COMMIT|BUILD_TIME)\b`)
download, build := -1, -1
args := map[string]int{}
for i, l := range lines {
if strings.HasPrefix(strings.TrimSpace(l), "FROM ") && i > 0 && download >= 0 && build >= 0 {
break // the runtime stage: only the builder matters
}
if m := argRe.FindStringSubmatch(l); m != nil {
if _, seen := args[m[1]]; !seen {
args[m[1]] = i // the FIRST declaration: any one above the download is the defect
}
}
if strings.Contains(l, "go mod download") && download < 0 {
download = i
}
if strings.Contains(l, "go build") && build < 0 {
build = i
}
}
if download < 0 || build < 0 {
t.Fatalf("the builder stage no longer has a `go mod download` (%d) or a `go build` (%d) — this test reads the wrong file", download, build)
}
for _, name := range []string{"VERSION", "GIT_COMMIT"} {
at, ok := args[name]
if !ok {
t.Errorf("ARG %s is not declared in the builder stage — the binary would be built without it", name)
continue
}
if at < download {
t.Errorf("R-208: ARG %s (line %d) is declared above `go mod download` (line %d), so every build with a new value re-downloads the modules", name, at+1, download+1)
}
if at > build {
t.Errorf("ARG %s (line %d) is declared after the `go build` that reads it (line %d)", name, at+1, build+1)
}
}
}
@@ -0,0 +1,58 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"testing"
"time"
)
// R-363 / R-547: the fill watcher ran once a day (plus once after start), so a filesystem that filled
// right after the 03:30 run went unannounced for ~24 h, and a disk held at 96 % for ten minutes raised
// nothing. The consequence pinned here: main.go registers the SAME fillWatcher.Check on a periodic
// job whose interval is no longer than the shortest measured silent window (10 min), so any such
// window contains a check. Edge-triggering (no repeat mails at a faster cadence) is pinned in
// internal/fillwatch (TestWarnsOnceThenIsSilent, TestEdgeStateSurvivesARestart).
func TestFillWatchRunsOnAnInterval(t *testing.T) {
const measuredSilentWindow = 10 * time.Minute // R-547, chaos night 2026-09-17
if fillWatchInterval <= 0 || fillWatchInterval > measuredSilentWindow {
t.Errorf("fillWatchInterval = %s; it must be >0 and <= %s, or a fill window like R-547's can pass unseen", fillWatchInterval, measuredSilentWindow)
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
periodic := 0
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Every" || len(call.Args) != 3 {
return true
}
iv, ok := call.Args[1].(*ast.Ident)
if !ok || iv.Name != "fillWatchInterval" {
return true
}
// The job body must call fillWatcher.Check — the same edge-triggered check, not a new one.
ast.Inspect(call.Args[2], func(m ast.Node) bool {
if c, ok := m.(*ast.CallExpr); ok {
if s, ok := c.Fun.(*ast.SelectorExpr); ok && s.Sel.Name == "Check" {
if x, ok := s.X.(*ast.Ident); ok && x.Name == "fillWatcher" {
periodic++
}
}
}
return true
})
return true
})
if periodic != 1 {
t.Errorf("R-363: main.go registers fillWatcher.Check on a periodic (sched.Every, fillWatchInterval) job %d times, want 1 — without it the fill check is daily only", periodic)
}
}