package apply import ( "context" "sort" "strings" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Strays is what runs on the machine that the mesh neither wrote nor holds (novox/hq ADR 0163): // every container the runtime has that no record names and no hold names. The question nothing // answered on 2026-09-23, when a renamed resource left its old container running for a day; asked // on every apply now, and reported, so a thing left behind is seen the day it is left. // // Containers only, today. A listener nobody declared is harder to attribute to a thing, and the // machine's own services are not strays; that account is issue 160's. func Strays(ctx context.Context, run Runner, known store.State) ([]store.Stray, error) { cri, err := containerRuntime(ctx, run) if err != nil { return nil, nil // a machine with no runtime has no containers to stray } out, err := run(ctx, cri, "ps", "-a", "--format", "{{.Names}}\t{{.Image}}\t{{.State}}") if err != nil { return nil, err } ours := map[string]bool{} for _, r := range known.Resources { if declaration.Type(r.Type) == declaration.TypeContainer { ours[r.Target] = true } } for _, h := range known.Held { if h.Kind == string(declaration.TypeContainer) { ours[h.Target] = true } } var strays []store.Stray for _, line := range strings.Split(strings.TrimSpace(out), "\n") { parts := strings.Split(line, "\t") name := strings.TrimSpace(parts[0]) if name == "" || ours[name] { continue } detail := "" if len(parts) > 2 { detail = strings.TrimSpace(parts[1]) + ", " + strings.TrimSpace(parts[2]) } strays = append(strays, store.Stray{Kind: string(declaration.TypeContainer), Name: name, Detail: detail}) } sort.Slice(strays, func(i, j int) bool { return strays[i].Name < strays[j].Name }) return strays, nil }