carlos / platform Public

rollout sim: a new record is every host's I9 fact; M17; seeds end their parked attempts (Task 2 fix round 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 43694ffe29a9b65be2f30ed3d8896fde2848448f parent 305cd54
3 files changed, +199 −63
  • internal/rollout/failures.go +1 −1
  • internal/rollout/sim_test.go +172 −57
  • internal/rollout/testdata/rolloutsim/mutate.sh +26 −5
diff --git a/internal/rollout/failures.go b/internal/rollout/failures.go
index 8e4d72155..2e46480e1 100644
--- a/internal/rollout/failures.go
+++ b/internal/rollout/failures.go
@@ -833,7 +833,7 @@ func (b *FailureBook) recordLocked(r FailureRecord) (RecordResult, bool) {
r.Retried, r.Epochs = nil, nil
// A new record is a new restriction of every host of the app (rule 1).
b.recs = append(b.recs, cloneRec(r))
- b.touchLocked(r.Account, r.App)
+ b.touchLocked(r.Account, r.App) // rolloutsim:M17
return Recorded, true
}
cur := &b.recs[i]
diff --git a/internal/rollout/sim_test.go b/internal/rollout/sim_test.go
index 995290fc0..f55457041 100644
--- a/internal/rollout/sim_test.go
+++ b/internal/rollout/sim_test.go
@@ -9,6 +9,7 @@ import (
"math/rand"
"os"
"path/filepath"
+ "runtime"
"runtime/debug"
"slices"
"sort"
@@ -100,14 +101,17 @@ import (
// - I9: a stale grant never clears a later failure (the crash-epoch hard
// limit, readiness round 2): when a signed `carlos instances retry`
// grant issued at step t clears anything (RetryHostAt), no unanswered
-// failure fact of the grant's (build, host) has a start after t. The
-// facts are the model's (sim.facts), never the book's verdicts: every
-// failure, hold-down and hold run the model delivers that changes the
-// model's own restriction state (sim.grec, the hold-ran restriction),
-// and every intent a restart reconciles. A fact is answered by a clear
-// the spec allows to lift it, issued after its start: a grant for its
-// (build, host), a pointer naming its build (a record), or a lift of
-// its host's hold-ran restriction (a hold run).
+// failure fact of the grant's (build, host) has a start after t (a fact
+// that began in t's own step is skipped). The facts are the model's
+// (sim.facts), never the book's verdicts: every failure, hold-down and
+// hold run the model delivers that changes the model's own restriction
+// state (sim.grec, the hold-ran restriction), and every intent a
+// restart reconciles. A failure that makes a new record is a fact of
+// every host of the app, wherever it ran, since a new record holds
+// every host (spec §8.4). A fact is answered by a clear the spec allows
+// to lift it, issued after its start: a grant for its build on the
+// grant's host, a pointer naming its build (a record), or a lift of its
+// host's hold-ran restriction (a hold run).
// - I10: durable before use: every non-zero generation PublishedEpoch
// returns is the app's generation in the book's last landed file,
// re-read from disk at that moment, under the identity the file holds.
@@ -119,7 +123,8 @@ import (
// re-reads its pending grants; agent restarts, which drop every execution
// in flight and reopen the book from disk under a new Manager; box
// rebuilds, which remove the book's file and open a new book; and a Retry
-// on an instance its hold-ran marker holds, followed by a wake stopped
+// on an instance its hold-ran marker holds, followed by a wake, of that
+// instance or of another that never served the marked build, stopped
// before it settles (rewakeArm). The grant queue and the facts outlive
// restarts and rebuilds.
//
@@ -162,6 +167,34 @@ func TestSimInvariants(t *testing.T) {
const simSteps = 400
+// TestSimSeedsEndTheirGoroutines: an agent restart drops the attempts parked
+// in Swap (agentRestart), and the seed's teardown (runSimSeed) must end their
+// goroutines. A goroutine left parked holds its sim, so a 3000-seed sweep
+// would keep every seed with a restart for the life of the process.
+func TestSimSeedsEndTheirGoroutines(t *testing.T) {
+ if os.Geteuid() == 0 {
+ t.Skip("as root no seed keeps its book on disk, so no seed restarts its agent")
+ }
+ opt, books := t.TempDir(), t.TempDir()
+ staged := map[string]bool{}
+ before := runtime.NumGoroutine()
+ // The seeds whose book is on disk (seed%4 == 3) are the ones that restart.
+ for seed := int64(3); seed < 16; seed += 4 {
+ if v := runSimSeed(seed, opt, books, staged, simSteps); v != nil {
+ t.Fatalf("rolloutsim: %s", v.line)
+ }
+ }
+ // An ended Manager's own goroutines leave on their cancelled context, a
+ // moment after it.
+ deadline := time.Now().Add(5 * time.Second)
+ for runtime.NumGoroutine() > before && time.Now().Before(deadline) {
+ time.Sleep(10 * time.Millisecond)
+ }
+ if after := runtime.NumGoroutine(); after > before {
+ t.Fatalf("%d goroutines before the seeds, %d after them", before, after)
+ }
+}
+
// The activator's first-wake window, as spawnPlanned reads it: B's health
// window ends firstWakeBudget less firstWakeKillReserve after the wake's
// start, and B is exec'd only with firstWakeMinVerdict of it left.
@@ -193,6 +226,13 @@ func runSimSeed(seed int64, opt, books string, staged map[string]bool, steps int
if s != nil && s.cancel != nil {
s.cancel()
}
+ if s != nil {
+ // Ends every attempt still parked in Swap (one an agent restart
+ // dropped, or one a violation left), without returning from Swap,
+ // so none of them touches the book or the model, and nothing holds
+ // the sim after the seed.
+ close(s.dead)
+ }
if s != nil && s.dir != "" {
// Writable again, so the seed's book can go (and t.TempDir's
// own cleanup never meets a read-only directory).
@@ -306,11 +346,14 @@ type simGrant struct {
// simFact is one of I9's failure facts: something the model delivered that
// restricts build on host (kind says what), by an execution that began at
-// start.
+// start. app is a new record, made by a failure on host: it holds every host
+// of the app (spec §8.4: "the build's next failure writes a new record,
+// which holds every host"), so the fact is every host's, not host's alone.
type simFact struct {
build, host, kind string
start time.Time
marker bool // a hold run: its host's hold-ran restriction
+ app bool // a new record: every host of the app
}
// simRec is the model's own failure record of a build (sim.grec): the hosts
@@ -419,12 +462,17 @@ type sim struct {
// retry clears and which are held down, so a failure is a fact only when
// it changes what the spec says restricts its host (plan finding 7: an
// unchanged record or a pre-retry outcome changes nothing). Kept from the
- // model's events; a record leaving the book (a pass) and a restart's
- // reread of the disk are the only things it takes from the book.
+ // model's events. It takes three things from the book: a record leaving
+ // it (a pass), a restart's reread of the disk, and a grant's clear, where
+ // deliver marks the host retried once the book's Lookup shows the record
+ // now lists it (a clear I9 has vetted first).
grec map[string]*simRec
- // rewake is the host rewakeArm granted a retry to, which rewakeRun wakes
- // and stops at the next step.
+ // rewake is the host rewakeRun wakes and stops at the next step: the one
+ // rewakeArm granted a retry to, or another on its channel.
rewake string
+ // rewakeFail is the build whose first wake on rewake fails its health
+ // check (another instance's wake, M17's witness), or "".
+ rewakeFail string
// late holds an ordinary wake's healthy event (GenHealthy, not a first
// wake) that the activator has not yet delivered, per host: its
@@ -437,6 +485,9 @@ type sim struct {
swaps map[string]*simSwap
events chan simEvent
draining bool
+ // dead is closed when the seed ends (runSimSeed): an attempt goroutine
+ // still parked in Swap, or about to report on events, exits instead.
+ dead chan struct{}
}
func newSim(seed int64, opt, books string, staged map[string]bool) *sim {
@@ -449,7 +500,7 @@ func newSim(seed int64, opt, books string, staged map[string]bool) *sim {
holdRanAt: map[string]time.Time{}, holdRanSet: map[string]time.Time{}, retryAt: map[string]map[string]time.Time{},
clearedAt: map[string]time.Time{}, failedAt: map[string]time.Time{}, late: map[string]simLate{}, swaps: map[string]*simSwap{},
issued: map[[2]string][]time.Time{}, signed: map[string][]time.Time{}, lifted: map[string][]time.Time{}, grec: map[string]*simRec{},
- events: make(chan simEvent)}
+ events: make(chan simEvent), dead: make(chan struct{})}
if seed%4 == 3 && os.Geteuid() != 0 {
// Root writes into a read-only directory, so it gets no faults, and
// none of I8, I9 and I10: its book stays in memory.
@@ -632,7 +683,10 @@ func (s *sim) failureOf(build, host string, stamp time.Time, heldDown bool, star
if reconciled {
kind = "an intent the restart reconciled"
}
- if s.recFailed(build, host, heldDown) || reconciled {
+ switch changed, created := s.recFailed(build, host, heldDown); {
+ case created:
+ s.facts = append(s.facts, simFact{build: build, host: host, kind: kind, start: start, app: true})
+ case changed || reconciled:
s.facts = append(s.facts, simFact{build: build, host: host, kind: kind, start: start})
}
s.checkI8(build, host, heldDown)
@@ -662,34 +716,39 @@ func (s *sim) stale(build, host string, start time.Time) bool {
}
// recFailed is a failure of build on host, not stale, in the model's own
-// record of build (grec), and reports whether it changed what restricts
-// host (I9): a new record (which restricts every host), host's retry spent,
-// or host newly held down. A failure that changes none of these moves no
-// generation (rule 1 in FailureBook's doc), so it is no fact; a newer
-// failure stamp alone is not counted either, the permissive direction.
-func (s *sim) recFailed(build, host string, heldDown bool) bool {
+// record of build (grec). It reports whether the failure made a new record
+// (created), which restricts every host of the app, and otherwise whether it
+// changed what restricts host (changed): host's retry spent, or host newly
+// held down (I9). A failure that changes none of these moves no generation
+// (rule 1 in FailureBook's doc), so it is no fact; a newer failure stamp
+// alone is not counted either, the permissive direction.
+func (s *sim) recFailed(build, host string, heldDown bool) (changed, created bool) {
r := s.grec[build]
- changed := r == nil || r.retried[host] || (heldDown && !r.down[host])
if r == nil {
r = &simRec{retried: map[string]bool{}, down: map[string]bool{}}
s.grec[build] = r
+ created = true
+ } else {
+ changed = r.retried[host] || (heldDown && !r.down[host])
}
delete(r.retried, host)
if heldDown {
r.down[host] = true
}
- return changed
+ return changed, created
}
-// answered reports whether a clear the spec allows to lift f was issued
-// after f's execution began (I9): a grant for its (build, host), a pointer
-// naming its build for a record, a lift of its host's hold-ran restriction
-// (a good write, a signed rollback) for a hold run.
-func (s *sim) answered(f simFact) bool {
+// answered reports whether a clear the spec allows to lift f, for host, was
+// issued after f's execution began (I9): a grant for its build on host, a
+// pointer naming its build for a record, a lift of its host's hold-ran
+// restriction (a good write, a signed rollback) for a hold run. host is f's
+// own host but for a new record (f.app), which holds every host: there it
+// is the host of the grant I9 checks, and only that host's grant answers it.
+func (s *sim) answered(f simFact, host string) bool {
after := func(ts []time.Time) bool {
return slices.ContainsFunc(ts, func(t time.Time) bool { return t.After(f.start) })
}
- if after(s.issued[[2]string{f.build, f.host}]) {
+ if after(s.issued[[2]string{f.build, host}]) {
return true
}
if f.marker {
@@ -809,11 +868,21 @@ func (f simAct) Owns(host string) bool {
return h != nil && h.phase != simAsleep
}
-// Swap parks the attempt until a complete step releases it.
+// Swap parks the attempt until a complete step releases it. When the seed
+// ends first (sim.dead), the goroutine exits from here: Swap never returns,
+// so done() never runs and the attempt's Manager never touches the book.
func (f simAct) Swap(_ context.Context, a activator.Attempt, newest func(string) uint64) activator.SwapResult {
sw := &simSwap{a: a, newest: newest, release: make(chan struct{})}
- f.s.events <- simEvent{host: a.Host, swap: sw}
- <-sw.release
+ select {
+ case f.s.events <- simEvent{host: a.Host, swap: sw}:
+ case <-f.s.dead:
+ runtime.Goexit()
+ }
+ select {
+ case <-sw.release:
+ case <-f.s.dead:
+ runtime.Goexit()
+ }
return f.s.finishSwap(sw)
}
@@ -881,8 +950,12 @@ func (s *sim) finishSwap(sw *simSwap) activator.SwapResult {
// An adoption onto a build the instance served once, and serves no
// more: not a failure the invariants count, but the code records
// it (done's transition), so the model's own record follows it
- // (I9), as no fact.
- s.recFailed(a.Build, a.Host, res.Outcome == activator.SwapDown)
+ // (I9). It is no fact for its own host, unless it made a new
+ // record: that holds every host of the app, so it is the app's
+ // fact (simFact.app).
+ if _, created := s.recFailed(a.Build, a.Host, res.Outcome == activator.SwapDown); created {
+ s.facts = append(s.facts, simFact{build: a.Build, host: a.Host, kind: "an adoption failure", start: sw.started, app: true})
+ }
}
if res.Outcome == activator.SwapFailed && a.Mode == activator.ModeHold && !sw.serves {
// A hold adoption that ran, failed and was not held down.
@@ -937,7 +1010,11 @@ func (s *sim) launch(d dispatch) {
if r := recover(); r != nil {
ev.crash, ev.stack = r, debug.Stack()
}
- s.events <- ev
+ select {
+ case s.events <- ev:
+ case <-s.dead:
+ // The seed ended (Swap's Goexit, or no one left to await).
+ }
}()
s.sched.execute(context.Background(), d)
}()
@@ -1405,12 +1482,14 @@ func (s *sim) checkPlanned(h *simHost, build string) {
// outcome reported through the real OnGen.
func (s *sim) wakeExec() {
if h := s.pick(func(h *simHost) bool { return h.phase == simWaking }); h != nil {
- s.wakeExecOn(h)
+ s.wakeExecOn(h, "")
}
}
-// wakeExecOn is wakeExec for h, which is waking.
-func (s *sim) wakeExecOn(h *simHost) {
+// wakeExecOn is wakeExec for h, which is waking. A first wake of failBuild
+// that runs fails its health check at once, as a flaky build may (rewakeRun's
+// scenario); "" fails nothing more than the build itself does.
+func (s *sim) wakeExecOn(h *simHost, failBuild string) {
rt, plan := h.planRoute, h.plan
if s.r.Intn(20) == 0 {
// The lease was refused (a previous incarnation still holds it): the
@@ -1498,8 +1577,9 @@ func (s *sim) wakeExecOn(h *simHost) {
return
}
b := s.builds[boot.Version]
- fails := b.bad || (b.flaky && s.r.Intn(2) == 0)
- if fails && s.r.Intn(2) == 0 {
+ forced := boot.Version == failBuild
+ fails := forced || b.bad || (b.flaky && s.r.Intn(2) == 0)
+ if fails && !forced && s.r.Intn(2) == 0 {
// It passed its health check and exits inside its observation.
fails, h.failLater = false, true
} else {
@@ -1804,7 +1884,17 @@ func (s *sim) retryHeld() {
// before another restriction of its app moves the generation; here the two
// halves are one step apart.
//
-// rewakeArm issues the grant and remembers the host.
+// When X has no failure record yet, half the time the wake is instead
+// another asleep instance's on the same channel, one that never served X,
+// and its first wake of X, if it runs, fails its health check (rewakeFail,
+// as a flaky build may: a build with a marker and no record has seldom
+// failed anywhere). That failure writes X's first record, which holds every
+// host of the app, the granted one too (spec §8.4): a fact of every host
+// after the grant was issued (M17's witness: a new record that moved no
+// generation). The book is read here only to pick the scenario, never for
+// a verdict.
+//
+// rewakeArm issues the grant and remembers the host to wake.
func (s *sim) rewakeArm() {
var c []*simHost
for _, h := range s.hosts {
@@ -1817,23 +1907,35 @@ func (s *sim) rewakeArm() {
return
}
h := c[s.gr.Intn(len(c))]
- s.issueGrant(h, s.installed[h.ch])
+ x := s.installed[h.ch]
+ s.issueGrant(h, x)
s.rewake = h.name
- s.logf("rewake %s: retry pressed under its hold-ran marker", h.name)
+ if _, recorded := s.book.Lookup("bab", "blog", x); !recorded {
+ var o []*simHost
+ for _, h2 := range s.hosts {
+ if h2 != h && h2.ch == h.ch && h2.phase == simAsleep && !h2.served[x] {
+ o = append(o, h2)
+ }
+ }
+ if len(o) > 0 && s.gr.Intn(2) == 0 {
+ s.rewake, s.rewakeFail = o[s.gr.Intn(len(o))].name, x
+ }
+ }
+ s.logf("rewake %s: retry pressed under its hold-ran marker; %s wakes next", h.name, s.rewake)
}
-// rewakeRun wakes the host rewakeArm armed, if it is still asleep: its plan
+// rewakeRun wakes the host rewakeArm chose, if it is still asleep: its plan
// and exec, then its stop while the first wake is under observation. Then
// the agent reads its grants.
func (s *sim) rewakeRun() {
- h := s.byName[s.rewake]
- s.rewake = ""
+ h, fail := s.byName[s.rewake], s.rewakeFail
+ s.rewake, s.rewakeFail = "", ""
if h.phase != simAsleep {
return
}
s.wakeBeginOn(h)
if h.phase == simWaking {
- s.wakeExecOn(h)
+ s.wakeExecOn(h, fail)
}
if _, inSwap := s.swaps[h.name]; h.phase == simObserving && !inSwap {
s.sleepOn(h)
@@ -1917,14 +2019,26 @@ func (s *sim) deliver(g simGrant) {
// checkI9 is I9 at a grant that has just cleared something: no failure fact
// of its (build, host) began after the grant was issued, unless a clear the
-// spec allows has answered it since.
+// spec allows has answered it since. A new record is a fact of every host of
+// the app (simFact.app), so one made by a failure on another host counts
+// too, answered only by a grant for g's build on g's host or a pointer for
+// the build. A fact that began in the grant's own step (a start not after
+// g.at) is skipped: the model's times are per step, so it cannot order the
+// two by time. A rewake's execution in that step began before the grant
+// (rewakeRun runs before retryHeld), and is rightly skipped; one the step's
+// own action started began after it, and skipping it is the permissive
+// direction.
func (s *sim) checkI9(g simGrant) {
for _, f := range s.facts {
- if f.build != g.build || f.host != g.host || !f.start.After(g.at) || s.answered(f) {
+ if f.build != g.build || (!f.app && f.host != g.host) || !f.start.After(g.at) || s.answered(f, g.host) {
continue
}
- violate("I9", "a grant for %s on %s issued at step %d (%s, book %.8s, generation %d) cleared it after %s of %s on %s in an execution begun at %s, which nothing has answered",
- g.build, g.host, g.step, g.at.Sub(t0), g.book, g.epoch, f.kind, f.build, f.host, f.start.Sub(t0))
+ what := ""
+ if f.app {
+ what = " (a new record, which holds every host)"
+ }
+ violate("I9", "a grant for %s on %s issued at step %d (%s, book %.8s, generation %d) cleared it after %s of %s on %s%s in an execution begun at %s, which nothing has answered",
+ g.build, g.host, g.step, g.at.Sub(t0), g.book, g.epoch, f.kind, f.build, f.host, what, f.start.Sub(t0))
}
}
@@ -1984,11 +2098,12 @@ func (s *sim) diskBook() (failureFile, error) {
}
// agentRestart is the agent process ending and starting again, on a seed
-// whose book is on disk (I9). Everything in flight goes with it: each
-// attempt parked in Swap is left there for good (its goroutine never runs
-// again, so nothing of the old Manager touches the book), a healthy event
-// on its way is lost, and every instance stops, the agent's children with
-// it. A window of failing saves closes first: an agent that cannot save
+// whose book is on disk (I9). Everything in flight goes with it. Each
+// attempt parked in Swap stays parked until the seed ends, when its
+// goroutine exits from Swap (sim.dead) without returning, so nothing of the
+// old Manager touches the book again, as a killed process cannot. A healthy
+// event on its way is lost, and every instance stops, the agent's children
+// with it. A window of failing saves closes first: an agent that cannot save
// its book at startup refuses managed starts (internal/host), so the box
// runs again only once the disk takes a save. The new agent opens the book
// from disk (OpenFailureBook, which reconciles the intents left there),
diff --git a/internal/rollout/testdata/rolloutsim/mutate.sh b/internal/rollout/testdata/rolloutsim/mutate.sh
index 16c0173c1..0e1b360b8 100755
--- a/internal/rollout/testdata/rolloutsim/mutate.sh
+++ b/internal/rollout/testdata/rolloutsim/mutate.sh
@@ -25,7 +25,7 @@
# The simulation's invariants are I1 to I10 (TestSimInvariants' doc). I9
# and I10 run only on the seeds whose failure book is on disk, which the
# simulation turns off when it runs as root (root writes into a read-only
-# directory, so it cannot fail a save): as root, M13 to M16 read MISSED.
+# directory, so it cannot fail a save): as root, M13 to M17 read MISSED.
#
# Execution integrity is recorded apart from detection. A run must build,
# start, and end in its own --- PASS or --- FAIL line for the test it ran
@@ -99,7 +99,7 @@ TIMEOUT_MIN=$((SEEDS * 3 / 100 + 20))
# build the host served before, under another build's marker, leaves
# it open, and that needs X installed again and adopted on a hold.
#
-# M13 to M16 are zero-downtime flag readiness round 2 task 2's (the
+# M13 to M17 are zero-downtime flag readiness round 2 task 2's (the
# crash-epoch hard limit), each on a seed whose book is on disk:
# M13 PublishedEpoch ignores its dirty check: I10. A change made while
# saves fail moves the generation in memory only; status publishes
@@ -123,8 +123,28 @@ TIMEOUT_MIN=$((SEEDS * 3 / 100 + 20))
# M16 RetryHostAt skips the book-identity comparison: I9. A box rebuild
# opens a new book whose generations start again from one, so a
# grant for the old book, still pending, matches a restriction the
-# new one made. The rarest of the four kills: at 300 seeds it rests
-# on a few seeds (rebuilds are drawn 1 step in 150).
+# new one made. One of the two rarest kills (with M17): at 300 seeds
+# it rests on a few seeds (rebuilds are drawn 1 step in 150). So a
+# MISSED for M16 at 300 seeds or fewer is not yet a regression: run
+# it again at 3000 seeds (mutate.sh 3000 M16) before reading it as
+# one.
+# M17 a new record moves the app's generation only from zero (the
+# sequence still advances, so nothing else changes): I9. A new
+# record holds every host of the app (spec §8.4), so a grant issued
+# under X's hold-ran marker on h, then a failure of X on another
+# host that writes X's new record, then the grant delivered: it
+# clears the marker and exempts h from the new record. I9 counts a
+# new record as a fact of every host. The random schedule seldom
+# lines this up (a build with a marker and no record has seldom
+# failed anywhere), so the simulation's rewakeArm makes it: when X
+# has no record, the wake after the Retry is sometimes another
+# instance's first wake of X, made to fail. Like M16 it rests on
+# few seeds at 300, and the same rule holds: a MISSED at 300 seeds
+# or fewer is re-run at 3000 before it is read as a regression.
+# (Removing the touch outright leaves the generation zero when the
+# new record is the app's first restriction, so every retry is
+# refused and I7 catches it first, which is not the hole this
+# mutant is for.)
MUTANTS='M1|queue.go|I2|s/if q\.running >= q\.concurrency \{/if false \&\& q.running >= q.concurrency {/
M2|queue.go|I3|s/class := classRecover/class := classAdopt/
M3|queue.go|I1|s/res, err = book\.Record\(rec\)/res, err = Recorded, nil/
@@ -140,7 +160,8 @@ M12|failures.go|test:TestAnotherExecutionOfTheMarkedBuildRenewsTheMarker|s/h\.Se
M13|failures.go|I10|s/if b\.dirty \{/if false \&\& b.dirty {/
M14|failures.go|I9|s/b\.touchLocked\(h\.Account, h\.App\)//
M15|failures.go|I9|s/b\.seq = f\.highestSaved\(\)/b.seq = f.Seq \/ 2/
-M16|failures.go|I9|s/ \|\| book != b\.idLocked\(\)//'
+M16|failures.go|I9|s/ \|\| book != b\.idLocked\(\)//
+M17|failures.go|I9|s/b\.touchLocked\(r\.Account, r\.App\)/if b.gens[genKey(r.Account, r.App)] == 0 { b.touchLocked(r.Account, r.App) } else { b.bumpLocked() }/'
# Every name selected must be a mutant in the list, checked before any work.
KNOWN=" $(printf '%s\n' "$MUTANTS" | cut -d'|' -f1 | tr '\n' ' ')"