carlos / platform Public

host: published layout intent distinguishes unreadable from none

startupLayoutIntent and layoutLegWith computed layout.IntentUnreadable
locally on a failed read or an unknown mode, but never published it:
layoutIntentSeen was left untouched, so a box whose every read failed
reported "" (none) even while recovery held a completed revert fenced
under exactly that intent. Publish the marker on every failure and
unknown-mode branch in both functions; a later successful read still
replaces it, and an absent or inapplicable intent still publishes "".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 82d225452da34cb026dcea9822f4ef2b8d952eb6 parent 945acac
2 files changed, +53 −8
  • internal/host/agent_layout_test.go +39 −3
  • internal/host/layoutleg.go +14 −5

CI passed — run details

diff --git a/internal/host/agent_layout_test.go b/internal/host/agent_layout_test.go
index 8d9aab777..f8f397952 100644
--- a/internal/host/agent_layout_test.go
+++ b/internal/host/agent_layout_test.go
@@ -82,7 +82,10 @@ func TestLayoutLegConvergesOnlyWhenTheIntentAppliesToThisBox(t *testing.T) {
}
// An intent this binary cannot read, or a request whose mode it does not
-// know, moves nothing, and the last intent it did read stands.
+// know, moves nothing, and publishes layout.IntentUnreadable — overwriting
+// whatever mode was last seen, since a read that failed just now is not
+// proof the fleet still wants that mode. A later successful read replaces
+// the marker the same way (TestStartupLayoutIntentPublishesUnreadable).
func TestLayoutLegDoesNothingWhenTheIntentIsUnreadable(t *testing.T) {
for _, tc := range []struct {
name string
@@ -108,8 +111,8 @@ func TestLayoutLegDoesNothingWhenTheIntentIsUnreadable(t *testing.T) {
if called {
t.Fatal("converged under an intent it could not read")
}
- if got := lastLayoutIntent(); got != release.LayoutModeFlat {
- t.Fatalf("intent seen = %q, want the last one read (flat)", got)
+ if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) {
+ t.Fatalf("intent seen = %q, want unreadable (the last read failed, not the last mode read)", got)
}
})
}
@@ -274,14 +277,23 @@ func TestStartupLayoutIntent(t *testing.T) {
if got := startupLayoutIntent(ctx, nil, errors.New("no bucket"), "kass-1"); got != layout.IntentUnreadable {
t.Fatalf("no store: %q", got)
}
+ if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) {
+ t.Fatalf("intent seen after no store = %q, want unreadable", got)
+ }
s := release.NewDirStore(t.TempDir())
if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != "" {
t.Fatalf("no object: %q", got)
}
+ if got := lastLayoutIntent(); got != "" {
+ t.Fatalf("intent seen after no object = %q, want none (a successful read replaces the marker)", got)
+ }
appendIntent(t, s, release.LayoutModeNested, "kass-2")
if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != "" {
t.Fatalf("another box's intent: %q", got)
}
+ if got := lastLayoutIntent(); got != "" {
+ t.Fatalf("intent seen after another box's intent = %q, want none", got)
+ }
appendIntent(t, s, release.LayoutModeNested, "")
if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != layout.ModeNested {
t.Fatalf("fleet intent: %q", got)
@@ -300,6 +312,30 @@ func TestStartupLayoutIntent(t *testing.T) {
if got := startupLayoutIntent(ctx, bad, nil, "kass-1"); got != layout.IntentUnreadable {
t.Fatalf("%s: %q", body, got)
}
+ // The prior call published "nested"; this failed read must overwrite
+ // it with the marker, not leave the stale mode standing.
+ if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) {
+ t.Fatalf("%s: intent seen = %q, want unreadable to overwrite the last known mode", body, got)
+ }
+ noteLayoutIntent(release.LayoutModeNested) // restore, for the next iteration's overwrite check
+ }
+}
+
+// TestStartupLayoutIntentPublishesUnreadable pins the fix on its own: a box
+// whose every read fails at startup must report layout.IntentUnreadable, not
+// "" (none) — recovery already holds a completed revert fenced under exactly
+// that intent, and the published status must not contradict it.
+func TestStartupLayoutIntentPublishesUnreadable(t *testing.T) {
+ resetLayoutGlobals(t)
+ ctx := context.Background()
+ if got := lastLayoutIntent(); got != "" {
+ t.Fatalf("intent seen before any read = %q, want none", got)
+ }
+ if got := startupLayoutIntent(ctx, nil, errors.New("no deploy store configured"), "kass-1"); got != layout.IntentUnreadable {
+ t.Fatalf("startupLayoutIntent = %q, want unreadable", got)
+ }
+ if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) {
+ t.Fatalf("intent seen = %q, want unreadable, not none", got)
}
}
diff --git a/internal/host/layoutleg.go b/internal/host/layoutleg.go
index cd018655c..caf6dff18 100644
--- a/internal/host/layoutleg.go
+++ b/internal/host/layoutleg.go
@@ -34,9 +34,11 @@ var armedLayoutPass atomic.Pointer[layout.Pass]
// a no-op and the edge's own 2s registry poll picks up a flipped row.
var layoutReload atomic.Pointer[func()]
-// layoutIntentSeen is the last layout intent mode the agent read for this
-// box: "" when none is recorded or none addresses it. A read that fails
-// leaves it as it was, because a failed read says nothing about the intent.
+// layoutIntentSeen is the last outcome of a layout intent read for this box:
+// "" when none is recorded or none addresses it, layout.IntentUnreadable
+// when the last read failed or named a mode this binary does not know. A
+// later successful read replaces whichever of the two it holds, so this
+// only ever needs to keep "no intent" apart from "we could not tell".
var layoutIntentSeen atomic.Pointer[string]
// lastLayoutIntent is the mode layoutIntentSeen holds, "" before any read.
@@ -137,6 +139,7 @@ const layoutIntentTimeout = 30 * time.Second
func startupLayoutIntent(ctx context.Context, store release.Store, storeErr error, boxLabel string) layout.Mode {
if storeErr != nil {
log.Printf("layout: intent -> unreadable: deploy store: %v", storeErr)
+ noteLayoutIntent(string(layout.IntentUnreadable))
return layout.IntentUnreadable
}
ctx, cancel := context.WithTimeout(ctx, layoutIntentTimeout)
@@ -145,12 +148,14 @@ func startupLayoutIntent(ctx context.Context, store release.Store, storeErr erro
switch {
case err != nil:
log.Printf("layout: intent -> unreadable: %v", err)
+ noteLayoutIntent(string(layout.IntentUnreadable))
return layout.IntentUnreadable
case !found:
noteLayoutIntent(release.LayoutModeNone)
return ""
case !release.ValidLayoutMode(in.Mode):
log.Printf("layout: intent %s -> unreadable: mode %q is not flat or nested", in.ID, in.Mode)
+ noteLayoutIntent(string(layout.IntentUnreadable))
return layout.IntentUnreadable
}
noteLayoutIntent(in.Mode)
@@ -220,19 +225,23 @@ func runLayoutLeg(ctx context.Context, store release.Store, boxLabel string) {
// layoutLegWith is the leg's decision: read the intent, and converge only
// when a request addresses this box with a mode this binary knows. An
-// intent that cannot be read moves nothing; the next serial move reads it
-// again.
+// intent that cannot be read moves nothing and publishes
+// layout.IntentUnreadable, overwriting whatever mode was last seen, since a
+// read that just failed is not proof the fleet still wants it; the next
+// serial move reads it again.
func layoutLegWith(ctx context.Context, store release.Store, boxLabel string, converge func(context.Context, layout.Mode) []layout.Result) {
in, found, err := release.LoadLayoutIntent(ctx, store, boxLabel)
switch {
case err != nil:
log.Printf("layout: intent -> unreadable: %v; not converging this move", err)
+ noteLayoutIntent(string(layout.IntentUnreadable))
return
case !found:
noteLayoutIntent(release.LayoutModeNone)
return
case !release.ValidLayoutMode(in.Mode):
log.Printf("layout: intent %s -> unreadable: mode %q is not flat or nested; not converging this move", in.ID, in.Mode)
+ noteLayoutIntent(string(layout.IntentUnreadable))
return
}
noteLayoutIntent(in.Mode)