carlos / platform Public

config sync: write a nested row's config.env as soon as its instance directory appears (defect 6)

Harness Run 9, finding 6. A nested instance created with settings has no
instance directory until its first wake (the activator's prepare makes it),
so config sync's write of <host>.config.env failed, and only the hourly pass
came back for it: the instance woke and ran without its settings, its store
binding included, for up to an hour.

SyncExec now names and skips a nested row whose write finds no instance
directory (errInstanceDirAbsent) instead of failing, so the pass goes on to
the other routes with claims on or off, and reports the host through
WhenInstanceDirAbsent. The agent keeps the last pass's waiting directories
(configDirsPending) and the 3 s generation loop runs the config pass again on
the first tick after one exists; the pass bounces the instance onto its
settings once the wake it is in has finished. Config sync still never makes
the directory itself: that stays the activator's.

The move's own path is unaffected: the landing renders config.env at the
target's flat paths before open (offline moves land flat rows only), and the
layout pass carries it into the instance directory when it nests the row.

Census: overlay plan14-root-reads-moves-offline-fixes.tsv, PROPOSED (agent
units only; proposal in docs/superpowers/notes/census-proposal-moves-offline-fixes.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ab78c51451047c45b0986099808171cd8e8d6c03 parent 54ae7fb
6 files changed, +330 −29
  • docs/superpowers/notes/census-proposal-moves-offline-fixes.md +42 −0
  • internal/appconfig/syncexec.go +36 −1
  • internal/host/agent.go +93 −28
  • internal/host/configdirpending_test.go +128 −0
  • internal/provision/plan14_root_reads_test.go +16 −0
  • internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv +15 −0
diff --git a/docs/superpowers/notes/census-proposal-moves-offline-fixes.md b/docs/superpowers/notes/census-proposal-moves-offline-fixes.md
new file mode 100644
index 000000000..9f2981b41
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-moves-offline-fixes.md
@@ -0,0 +1,42 @@
+# Census: the offline move's harness fixes (Run 9, defects 6 to 8)
+
+Status: PROPOSED. Every row carries `rev3=PROPOSED-moves-offline-fixes`. The
+ruling is asked of the census owner, platform-1c.
+
+The branch is `moves-offline-fixes`, from main after 54ae7fb9 (v0.28.5). It
+fixes three defects the deployment harness found in the offline operator move
+(harness RUNLOG, Run 9). It adds one census overlay,
+`internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv`,
+loaded last in `TestPlan14RootReadCensus`, after armed-box-updates. The
+record-write census (`TestPlan9B8RecordWriteCensus`) does not change: no
+record write is added or moved.
+
+## The footing
+
+Every unit in the overlay runs in the box agent (`/opt/carlos/carlos agent`,
+carlos-edge.service, `User=carlos`). None is reached from a root process, and
+none makes a root decision. So each is proposed accepted legacy at the
+disposition main gives it or its siblings: J1 (root-route) for registry and
+route reads, J4 (root-input) for file reads, J1,J4 where a unit does both.
+That is the footing census-ruling-moves-offline accepted for the offline
+move's own units.
+
+## The units, by defect
+
+### Defect 6: config.env for a nested row whose instance directory is not made yet
+
+| unit | change | executing identity | disposition |
+|---|---|---|---|
+| `internal/appconfig/syncexec.go` `SyncExecWithCallbacks` | changed legacy: a write that found no instance directory is named and skipped (`errInstanceDirAbsent`, `WhenInstanceDirAbsent`), and the pass goes on; no new read site | the agent, through `syncExecSerialized` (config loops) | re-frozen, J1,J4 |
+| `internal/appconfig/syncexec.go` `renderExecConfigBefore` | changed legacy: one `Lstat` of the nested row's instance directory after its write failed, to tell "not made yet" from any other failure; follows no link | the agent: config sync and the moves worker's held render | re-frozen, J1,J4 |
+| `internal/host/agent.go` `configGenLoop` | changed legacy: the tick's body moved into `configGenTick`; the registry listing stays | the agent | re-frozen, J1 |
+| `internal/host/agent.go` `configGenTick` | new: `configGenLoop`'s tick over the routes it listed; also runs the agent's own config pass once a directory the last pass waited for exists | the agent | accepted legacy, J1 (as `configGenLoop`) |
+| `internal/host/agent.go` `pendingDirs.appeared` | new: an `Lstat` (never follows a link) of the instance directories the last config pass could not write into | the agent | accepted legacy, J4 (as the moves units' `isDir`, `lexists`) |
+
+Envelopes re-pinned: `internal/appconfig`, `internal/host`.
+
+## The seal
+
+The accepted-legacy rows add frozen declarations, so the legacy seal moves.
+The test pins the new value after the overlay loads; main's seal is still
+checked first, so a change on main's side shows on its own.
diff --git a/internal/appconfig/syncexec.go b/internal/appconfig/syncexec.go
index e693235a0..b02382b4d 100644
--- a/internal/appconfig/syncexec.go
+++ b/internal/appconfig/syncexec.go
@@ -635,7 +635,21 @@ func SyncExecWithCallbacks(ctx context.Context, reg *registry.Registry, s releas
hostCallbacks.Delivered = nil
}
}
- if err := syncExecHostClaimed(ctx, selected[rt.Host], rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, instETag, ack, hostCallbacks); err != nil && hostWrites.Enabled {
+ err := syncExecHostClaimed(ctx, selected[rt.Host], rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, instETag, ack, hostCallbacks)
+ // A nested row's bundle lives in its instance directory, which the
+ // activator makes at the first wake. Before then the write cannot
+ // land, so the row is named and the pass goes on to the other
+ // routes, claims on or off; the agent runs the pass again once the
+ // directory is there (WhenInstanceDirAbsent). A render that failed
+ // fails as before, directory or not.
+ if errors.Is(err, errInstanceDirAbsent) {
+ fmt.Printf(" %s: instance directory not made yet; config is written once it is\n", rt.Host)
+ if o.dirAbsent != nil {
+ o.dirAbsent(rt.Host, filepath.Dir(ConfigEnvPath(dataDir, rt)))
+ }
+ continue
+ }
+ if err != nil && hostWrites.Enabled {
return err
}
}
@@ -726,10 +740,24 @@ type SyncOption func(*syncOptions)
type syncOptions struct {
fenced func(host string) bool
+ dirAbsent func(host, dir string)
hostWrites hostwrite.Config
hostWriteCount int
}
+// WhenInstanceDirAbsent hands SyncExec a callback for every nested row whose
+// bundle it could not write because the row's instance directory does not
+// exist yet: the activator makes that directory at the instance's first
+// wake (prepare), so a nested row created with settings has nowhere to put
+// its <host>.config.env until then. The pass skips such a row cleanly, says
+// so, and calls dirAbsent with the host and the directory it waits for, so
+// the agent can run the pass again as soon as the directory appears rather
+// than leaving the instance without its settings until the hourly floor
+// (harness Run 9, finding 6).
+func WhenInstanceDirAbsent(dirAbsent func(host, dir string)) SyncOption {
+ return func(o *syncOptions) { o.dirAbsent = dirAbsent }
+}
+
func WithHostWrite(cfg hostwrite.Config) SyncOption {
return func(o *syncOptions) {
o.hostWrites = cfg
@@ -1311,6 +1339,10 @@ func syncExecHostClaimed(ctx context.Context, work hostwrite.Work, rt registry.R
return nil
}
+// errInstanceDirAbsent is a nested row's write that found no instance
+// directory: the activator has not woken the instance here yet.
+var errInstanceDirAbsent = errors.New("the instance directory is not made yet")
+
// execRender is what renderExecConfig did: the file's path, the bytes it
// holds now, whether this call wrote them, and the identity the writes were
// guarded under (zero when nothing was written).
@@ -1433,6 +1465,9 @@ func renderExecConfigBefore(ctx context.Context, work hostwrite.Work, rt registr
// temp-then-rename it always used.
if nestedRoute(dataDir, rt) {
if err := guarded(ctx, work, id, func() error { return instancefs.WriteAtomic(filepath.Dir(path), filepath.Base(path), rendered, 0o600) }); err != nil {
+ if _, lerr := os.Lstat(filepath.Dir(path)); errors.Is(lerr, os.ErrNotExist) {
+ return execRender{}, fmt.Errorf("%w: %s", errInstanceDirAbsent, filepath.Dir(path))
+ }
fmt.Printf(" %s: write %s: %v\n", rt.Host, path, err)
return execRender{}, err
}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index ce93fb963..1bd1078f8 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -1375,17 +1375,68 @@ var configSyncMu sync.Mutex
func syncExecSerialized(ctx context.Context, reg *registry.Registry, store release.Store, priv *ecdh.PrivateKey, dd string, svc *activator.Service, writes ...hostwrite.Config) error {
configSyncMu.Lock()
defer configSyncMu.Unlock()
- opts := []appconfig.SyncOption{appconfig.SkipFenced(func(host string) bool { return layout.JournalPresent(dd, host) })}
+ pending := map[string]string{}
+ opts := []appconfig.SyncOption{
+ appconfig.SkipFenced(func(host string) bool { return layout.JournalPresent(dd, host) }),
+ appconfig.WhenInstanceDirAbsent(func(host, dir string) { pending[host] = dir }),
+ }
for _, cfg := range writes {
opts = append(opts, appconfig.WithHostWrite(cfg))
}
// CARLOS_SWAP=overlap: the scheduler restarts, paced, and acknowledges a
// revision once it is active; refusing: no restart. configSyncMu above
// is what makes ConfigDelivered serial per host.
+ var err error
if cb, ok := rolloutConfigCallbacks(ctx, store, configLegs{awake: managedAwake(svc, reg), restart: bounce(svc)}); ok {
- return appconfig.SyncExecWithCallbacks(ctx, reg, store, priv, dd, os.Getenv("PLATFORM_APPS_DOMAIN"), cb, opts...)
+ err = appconfig.SyncExecWithCallbacks(ctx, reg, store, priv, dd, os.Getenv("PLATFORM_APPS_DOMAIN"), cb, opts...)
+ } else {
+ err = appconfig.SyncExec(ctx, reg, store, priv, dd, bounce(svc), os.Getenv("PLATFORM_APPS_DOMAIN"), configAckFunc(ctx, store, "agent"), opts...)
}
- return appconfig.SyncExec(ctx, reg, store, priv, dd, bounce(svc), os.Getenv("PLATFORM_APPS_DOMAIN"), configAckFunc(ctx, store, "agent"), opts...)
+ configDirsPending.record(pending, err == nil)
+ return err
+}
+
+// configDirsPending is what the last config pass could not write because a
+// nested row's instance directory did not exist yet (harness Run 9, finding
+// 6): host -> the directory. The activator makes that directory at the
+// first wake, and before this the only pass that came back for the file was
+// configSyncLoop's hourly one, so a nested instance created with settings
+// woke and ran without them, its store binding included, for up to an hour.
+// configGenLoop now looks at these directories on every tick and runs the
+// pass again as soon as one is there.
+var configDirsPending pendingDirs
+
+type pendingDirs struct {
+ mu sync.Mutex
+ dirs map[string]string
+}
+
+// record keeps what one pass found. Every pass sweeps every route, so a pass
+// that completed replaces the set; one that failed part way only adds to it,
+// since a host it never reached may still be waiting.
+func (p *pendingDirs) record(found map[string]string, complete bool) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ if complete || p.dirs == nil {
+ p.dirs = map[string]string{}
+ }
+ for host, dir := range found {
+ p.dirs[host] = dir
+ }
+}
+
+// appeared names a host whose instance directory the last pass waited for
+// and which exists now; ok is false when there is none. Lstat only: the
+// directory is the activator's, and nothing here follows or opens it.
+func (p *pendingDirs) appeared() (host string, ok bool) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ for h, dir := range p.dirs {
+ if _, err := os.Lstat(dir); err == nil {
+ return h, true
+ }
+ }
+ return "", false
}
// configAckFunc builds the callback config sync's two passes (the root
@@ -1906,33 +1957,47 @@ func configGenLoop(reg *registry.Registry, svc *activator.Service, dd string, wr
log.Printf("agent: config generation poll: list routes: %v", err)
continue
}
- execChanged, unitChanged, readErrs := genPollPass(ctx, routes, store, cache)
- if len(readErrs) > 0 {
- // ONE line per pass, never one per app: a bucket having a bad
- // minute would otherwise flood the journal at 20 lines a minute
- // per app. The count carries the scale, the first error carries
- // the cause; they are the same cause in practice.
- log.Printf("agent: config generation poll: %d apps unreadable: %v", len(readErrs), readErrs[0])
- }
- if unitChanged {
- // Requested first and never fatal: the write is instant while
- // syncExecSerialized can block on a serialized pass, so the root
- // run gets to start alongside it rather than behind it. A failed
- // marker write costs those apps the wait until
- // carlos-adopt.timer's next hourly pass — never correctness.
- if err := requestRootSync(dd); err != nil {
- log.Printf("agent: request root config sync: %v", err)
- } else {
- log.Printf("agent: config changed for unit-backed app(s), requested root config sync pass")
- }
- }
- if !execChanged {
- continue
- }
- if err := syncExecSerialized(ctx, reg, store, priv, dd, svc, writes); err != nil {
- log.Printf("agent: config generation sync: %v", err)
+ configGenTick(ctx, routes, reg, store, priv, dd, svc, writes, cache)
+ }
+}
+
+// configGenTick is one tick of configGenLoop over the routes it listed, split
+// out so a test can run the loop's own decision.
+func configGenTick(ctx context.Context, routes []registry.Route, reg *registry.Registry, store release.Store, priv *ecdh.PrivateKey, dd string, svc *activator.Service, writes hostwrite.Config, cache *genCache) {
+ execChanged, unitChanged, readErrs := genPollPass(ctx, routes, store, cache)
+ if len(readErrs) > 0 {
+ // ONE line per pass, never one per app: a bucket having a bad
+ // minute would otherwise flood the journal at 20 lines a minute
+ // per app. The count carries the scale, the first error carries
+ // the cause; they are the same cause in practice.
+ log.Printf("agent: config generation poll: %d apps unreadable: %v", len(readErrs), readErrs[0])
+ }
+ if unitChanged {
+ // Requested first and never fatal: the write is instant while
+ // syncExecSerialized can block on a serialized pass, so the root
+ // run gets to start alongside it rather than behind it. A failed
+ // marker write costs those apps the wait until
+ // carlos-adopt.timer's next hourly pass — never correctness.
+ if err := requestRootSync(dd); err != nil {
+ log.Printf("agent: request root config sync: %v", err)
+ } else {
+ log.Printf("agent: config changed for unit-backed app(s), requested root config sync pass")
}
}
+ // A nested row an earlier pass could not write for because its instance
+ // directory was not there yet: the first wake has made it, so its
+ // bundle goes in now (finding 6). The pass bounces the instance onto
+ // its settings once the wake it is in has finished (Service.Bounce).
+ if host, ok := configDirsPending.appeared(); ok && !execChanged {
+ log.Printf("agent: %s: its instance directory is there now, writing its config", host)
+ execChanged = true
+ }
+ if !execChanged {
+ return
+ }
+ if err := syncExecSerialized(ctx, reg, store, priv, dd, svc, writes); err != nil {
+ log.Printf("agent: config generation sync: %v", err)
+ }
}
// Fast-converge cadence and clamps (carlos-deploy spec §3-§4). The tick is
diff --git a/internal/host/configdirpending_test.go b/internal/host/configdirpending_test.go
new file mode 100644
index 000000000..dc3d1639d
--- /dev/null
+++ b/internal/host/configdirpending_test.go
@@ -0,0 +1,128 @@
+package host
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/appconfig"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Harness Run 9, finding 6: a nested instance created with settings has no
+// instance directory until its first wake, so config sync's write of its
+// <host>.config.env failed, and only the hourly pass came back for it. The
+// instance woke and ran without its settings (its store binding included).
+//
+// Through the agent's own wiring: the boot or hourly pass (syncExecSerialized)
+// skips the row and names it, while a sibling whose directory exists gets its
+// file in the same pass; the generation loop's tick (configGenTick) does
+// nothing while the directory is absent, and on the first tick after the
+// first wake has made it, writes the file.
+func TestConfigGenTickWritesANestedRowsConfigOnceItsDirAppears(t *testing.T) {
+ configDirsPending.record(nil, true)
+ t.Cleanup(func() { configDirsPending.record(nil, true) })
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ reg := restartTestRegistry(t)
+ dd := t.TempDir()
+ privText, _, err := appconfig.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ priv, err := appconfig.ParsePrivateKey(privText, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := appconfig.SetEnv(ctx, s, "bab", "shop", map[string]string{"STORE_BUCKET": "store-shop"}); err != nil {
+ t.Fatal(err)
+ }
+ fresh, woken := "shop.example.com", "older.example.com"
+ for _, h := range []string{fresh, woken} {
+ if err := reg.Upsert(registry.Route{Host: h, Kind: "instance", App: "shop", Account: "bab", Hibernate: true,
+ Status: "ready", DBPath: provision.NestedDBPath(dd, h), Socket: provision.NestedSocket(h)}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // The sibling has woken before: the activator's prepare made its
+ // instance directory (os.MkdirAll(dataDir, 0o700), activator/prepare.go).
+ firstWake := func(host string) {
+ t.Helper()
+ if err := os.MkdirAll(provision.InstanceDir(dd, host), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ firstWake(woken)
+ cfgPath := func(h string) string { return filepath.Join(provision.InstanceDir(dd, h), h+".config.env") }
+
+ // The boot pass (configSyncLoop's first, a minute after start).
+ if err := syncExecSerialized(ctx, reg, s, priv, dd, nil, hostwrite.Config{}); err != nil {
+ t.Fatalf("a nested row with no directory failed the pass: %v", err)
+ }
+ if b, err := os.ReadFile(cfgPath(woken)); err != nil || string(b) != "STORE_BUCKET=store-shop\n" {
+ t.Fatalf("the sibling's bundle = %q, %v", b, err)
+ }
+ if _, err := os.Lstat(provision.InstanceDir(dd, fresh)); !os.IsNotExist(err) {
+ t.Fatalf("config sync made the instance directory itself (err %v): that is the activator's", err)
+ }
+
+ // The generation loop: the first tick seeds its cache, and no tick
+ // writes while the directory is absent.
+ cache := newGenCache()
+ tick := func() {
+ t.Helper()
+ routes, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ configGenTick(ctx, routes, reg, s, priv, dd, nil, hostwrite.Config{}, cache)
+ }
+ for range 2 {
+ tick()
+ }
+ if _, err := os.Lstat(cfgPath(fresh)); !os.IsNotExist(err) {
+ t.Fatalf("a bundle with nowhere to go: %v", err)
+ }
+
+ // The first wake makes the directory; the next tick writes the bundle,
+ // with no config change and no hourly pass.
+ firstWake(fresh)
+ tick()
+ if b, err := os.ReadFile(cfgPath(fresh)); err != nil || string(b) != "STORE_BUCKET=store-shop\n" {
+ t.Fatalf("after the first wake, the bundle = %q, %v", b, err)
+ }
+ if _, ok := configDirsPending.appeared(); ok {
+ t.Fatal("the written host is still waited for")
+ }
+ // Nothing more to wait for: a later tick does not run the pass again.
+ if err := os.Remove(cfgPath(fresh)); err != nil {
+ t.Fatal(err)
+ }
+ tick()
+ if _, err := os.Lstat(cfgPath(fresh)); !os.IsNotExist(err) {
+ t.Fatalf("a tick with nothing pending ran the pass (err %v)", err)
+ }
+}
+
+// A pass that fails part way does not forget a host an earlier pass was
+// waiting for; a complete pass replaces the set.
+func TestConfigDirsPendingKeepsWhatAFailedPassDidNotReach(t *testing.T) {
+ var p pendingDirs
+ dir := filepath.Join(t.TempDir(), "instances", "shop.example.com")
+ p.record(map[string]string{"shop.example.com": dir}, true)
+ p.record(map[string]string{}, false)
+ if err := os.MkdirAll(dir, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if h, ok := p.appeared(); !ok || h != "shop.example.com" {
+ t.Fatalf("appeared = %q %v after a failed pass", h, ok)
+ }
+ p.record(map[string]string{}, true)
+ if h, ok := p.appeared(); ok {
+ t.Fatalf("appeared = %q after a complete pass found nothing", h)
+ }
+}
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 95983645d..40809ef76 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -981,6 +981,22 @@ func TestPlan14RootReadCensus(t *testing.T) {
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-armed-box-updates.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
+ // Census overlay moves-offline-fixes (harness Run 9's defects 6 to 8 in
+ // the offline operator move), loaded last. PROPOSED-moves-offline-fixes,
+ // ruling asked of the census owner (platform-1c); the proposal names
+ // each unit's executing identity
+ // (docs/superpowers/notes/census-proposal-moves-offline-fixes.md). Every
+ // unit runs in the agent and makes no root decision: changed legacy
+ // units re-frozen at main's disposition, new ones proposed accepted
+ // legacy at their siblings'. Frozen declarations are added, so the seal
+ // moves; main's is checked first.
+ if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 {
+ t.Fatal(errs)
+ }
+ if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv"), &inv, true); err != nil {
+ t.Fatal(err)
+ }
+ seal = "247e7d2d5e9213e08e2d631d3416c2fe2ca3044f9ea8c70d3e515139a822d007"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv b/internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv
new file mode 100644
index 000000000..842946e9b
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv
@@ -0,0 +1,15 @@
+# Census overlay moves-offline-fixes (harness Run 9's defects 6 to 8 in the offline operator move, on main after 54ae7fb9), loaded LAST, after armed-box-updates. PROPOSED-moves-offline-fixes: every row is rev3=PROPOSED-moves-offline-fixes, and the ruling is asked of the census owner (platform-1c). The proposal, with each unit's executing identity: docs/superpowers/notes/census-proposal-moves-offline-fixes.md.
+# Every unit here runs in the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos) and makes no root decision, so each is proposed accepted legacy at main's disposition (J1 root-route for registry and route reads, J4 root-input for file reads), as census-ruling-moves-offline proposed its units.
+# Defect 6 (config.env for a nested row whose instance directory is not made yet): SyncExecWithCallbacks, renderExecConfigBefore and configGenLoop re-frozen at main's disposition; configGenTick and pendingDirs.appeared new. The seal moves.
+@coverage internal/appconfig 2c32778cc777f026f1dd7b6404b11e3fabf1b6bd4912799d6ef26bfb59d42a07
+@coverage internal/host e0ca6a7c7d5d4f1d167e77c34c61795b1f35dcf60b8a2c8209340758b59a106a
+@legacy 783751049991bdd17a140bba6ce9a3049a4075b1170e4882c7207b20bed24b41 internal/appconfig/syncexec.go SyncExecWithCallbacks legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=route-and-input (J1,J4, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), through syncExecSerialized: main's body, with a nested row whose write found no instance directory named and skipped (errInstanceDirAbsent, WhenInstanceDirAbsent) instead of failing the pass; no new read site file:Stat@28119,registry:App@10156,registry:App@10371,registry:App@15749,registry:App@28004,registry:Environment@10164,registry:Environment@10379,registry:Environment@15757,registry:Environment@28012,registry:Host@1031,registry:Host@1460,registry:Host@15584,registry:Host@15682,registry:Host@16514,registry:Host@16865,registry:Host@17800,registry:Host@18341,registry:Host@21268,registry:Host@21485,registry:Host@22043,registry:Host@22233,registry:Host@22242,registry:Host@22261,registry:Host@25162,registry:Host@25779,registry:Host@25831,registry:Host@28513,registry:Host@831,registry:Host@902,registry:Host@996,whole:Route@10049,whole:Route@10110,whole:Route@10151,whole:Route@10268,whole:Route@10302,whole:Route@10366,whole:Route@1412,whole:Route@1441,whole:Route@15193,whole:Route@15243,whole:Route@15744,whole:Route@24758,whole:Route@25172,whole:Route@25876,whole:Route@27903,whole:Route@27936,whole:Route@27999,whole:Route@28100,whole:Route@28750,whole:Route@28823,whole:Route@693,whole:Route@721,whole:Route@750,whole:Route@776,whole:Route@9984 8d58864244e2a823103fab97653d48b7d586843eee5fcb5848194447c2f0210f
+internal/appconfig/syncexec.go SyncExecWithCallbacks legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=route-and-input (J1,J4, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), through syncExecSerialized: main's body, with a nested row whose write found no instance directory named and skipped (errInstanceDirAbsent, WhenInstanceDirAbsent) instead of failing the pass; no new read site file:Stat@28119,registry:App@10156,registry:App@10371,registry:App@15749,registry:App@28004,registry:Environment@10164,registry:Environment@10379,registry:Environment@15757,registry:Environment@28012,registry:Host@1031,registry:Host@1460,registry:Host@15584,registry:Host@15682,registry:Host@16514,registry:Host@16865,registry:Host@17800,registry:Host@18341,registry:Host@21268,registry:Host@21485,registry:Host@22043,registry:Host@22233,registry:Host@22242,registry:Host@22261,registry:Host@25162,registry:Host@25779,registry:Host@25831,registry:Host@28513,registry:Host@831,registry:Host@902,registry:Host@996,whole:Route@10049,whole:Route@10110,whole:Route@10151,whole:Route@10268,whole:Route@10302,whole:Route@10366,whole:Route@1412,whole:Route@1441,whole:Route@15193,whole:Route@15243,whole:Route@15744,whole:Route@24758,whole:Route@25172,whole:Route@25876,whole:Route@27903,whole:Route@27936,whole:Route@27999,whole:Route@28100,whole:Route@28750,whole:Route@28823,whole:Route@693,whole:Route@721,whole:Route@750,whole:Route@776,whole:Route@9984 8d58864244e2a823103fab97653d48b7d586843eee5fcb5848194447c2f0210f
+@legacy 356c576628a285021657e989a015ef938bb1da3498d8146a734903a23fea1e4a internal/appconfig/syncexec.go renderExecConfigBefore legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=route-and-input (J1,J4, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from config sync and the moves worker's held render: main's body with one Lstat of a nested row's instance directory after its write failed, to tell a directory not made yet from any other failure; it follows no link and changes no outcome but the error's kind file:Lstat@3893,file:ReadFile@951,registry:Environment@3190,registry:Host@1070,registry:Host@2586,registry:Host@4088,registry:Host@4320,registry:Host@4576,registry:Host@4669,registry:Host@876,whole:Route@2522,whole:Route@257,whole:Route@3715,whole:Route@680 a60a9a2e2158f3a8dadf6fcf46810930dad89b67d1acc23fb57c324a7d9ac1f1
+internal/appconfig/syncexec.go renderExecConfigBefore legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=route-and-input (J1,J4, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos), from config sync and the moves worker's held render: main's body with one Lstat of a nested row's instance directory after its write failed, to tell a directory not made yet from any other failure; it follows no link and changes no outcome but the error's kind file:Lstat@3893,file:ReadFile@951,registry:Environment@3190,registry:Host@1070,registry:Host@2586,registry:Host@4088,registry:Host@4320,registry:Host@4576,registry:Host@4669,registry:Host@876,whole:Route@2522,whole:Route@257,whole:Route@3715,whole:Route@680 a60a9a2e2158f3a8dadf6fcf46810930dad89b67d1acc23fb57c324a7d9ac1f1
+@legacy 547dba02dd41e9e318da6368d9bf493f94152a4d5e56737765199cc2f298a302 internal/host/agent.go configGenLoop legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-route (J1, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's loop, its tick's body moved into configGenTick; the registry listing stays here whole:Route@1265 a0b36186f7c8abdd818c81c2651a75b202078ad7f197d93b6157bcab97604188
+internal/host/agent.go configGenLoop legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-route (J1, main's disposition); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's loop, its tick's body moved into configGenTick; the registry listing stays here whole:Route@1265 a0b36186f7c8abdd818c81c2651a75b202078ad7f197d93b6157bcab97604188
+@legacy 2bf1eb09b85c6c8fa62feb2173628e757913e153a656247a3423ef9a753783b0 internal/host/agent.go configGenTick legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-route (J1, as configGenLoop); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, configGenLoop's tick over the routes it listed; it also runs the agent's own config pass once an instance directory the last pass waited for exists whole:Route@268 751870d636c3ea7b3670a7e00c71d085e94ae9b013545e319047a4609f514943
+internal/host/agent.go configGenTick legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-route (J1, as configGenLoop); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, configGenLoop's tick over the routes it listed; it also runs the agent's own config pass once an instance directory the last pass waited for exists whole:Route@268 751870d636c3ea7b3670a7e00c71d085e94ae9b013545e319047a4609f514943
+@legacy 237af935c475d587a83b4e7a33d21e1f3fd32f36333a8440b9f3b44b21b8509e internal/host/agent.go pendingDirs.appeared legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-input (J4, as the moves units' isDir and lexists); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, an Lstat that never follows a link, of the instance directories the last config pass could not write into; it decides only whether the agent runs its own config pass again file:Lstat@137 bc87759cf499661d1058712b03230924e1669a8a1710b88e4053c8b4f7bd3aa3
+internal/host/agent.go pendingDirs.appeared legacy accepted-legacy accepted-legacy;rev3=PROPOSED-moves-offline-fixes;basis=root-input (J4, as the moves units' isDir and lexists); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, an Lstat that never follows a link, of the instance directories the last config pass could not write into; it decides only whether the agent runs its own config pass again file:Lstat@137 bc87759cf499661d1058712b03230924e1669a8a1710b88e4053c8b4f7bd3aa3