carlos / platform Public

layout: a journal held under a move's fence says how to get out (plan 4 final review)

The cut waits for the journal and the journal waits for the cut's fence.
The reason now names the instance move and the two ways out: abort the
move, or remove the journal now that the row stands at its destination.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9459d59b80950a15641f8e3780818b7e62d87404 parent ee6f2a6
2 files changed, +15 −1
  • internal/layout/pass.go +5 −1
  • internal/layout/pass_linux_test.go +10 −0
diff --git a/internal/layout/pass.go b/internal/layout/pass.go
index 349a00c8c..0b0fedfce 100644
--- a/internal/layout/pass.go
+++ b/internal/layout/pass.go
@@ -689,7 +689,11 @@ func (p *Pass) recoverHost(ctx context.Context, host string, byHost map[string]r
var holdReason string
switch {
case fence == fenceHeld && intent != j.Mode:
- holdReason = fmt.Sprintf("completed to %s under another operation's fence; journal kept at done until that fence is lifted", j.Mode)
+ // Only an instance move shares the fence slot, and its cut waits
+ // for this journal to go while the pass waits for the move's
+ // fence to lift. Neither goes on alone, so the reason names both
+ // ways out for whoever reads the agent log.
+ holdReason = fmt.Sprintf("completed to %s under an instance move's fence; journal kept at done. the move waits for this journal and the journal waits for the move. abort the move, or remove %s now that the row stands at its destination, and the move can finish", j.Mode, provision.LayoutJournalPath(p.DataDir, host))
case intent == IntentUnreadable && j.Mode == ModeFlat:
// Releasing a completed revert flat before the intent is known is
// the forged-revert window again.
diff --git a/internal/layout/pass_linux_test.go b/internal/layout/pass_linux_test.go
index 6c94909c9..a82dd1354 100644
--- a/internal/layout/pass_linux_test.go
+++ b/internal/layout/pass_linux_test.go
@@ -861,6 +861,16 @@ func TestRecoverUnderAForeignFenceKeepsTheJournalWhenNotAtTheIntent(t *testing.T
if r := res[host]; r.Outcome != "deferred" || !strings.Contains(r.Reason, "fence") {
t.Fatalf("held host: %+v", r)
}
+ // The move waits for the journal and the journal waits for the move,
+ // so the reason has to tell an operator both ways out.
+ for _, want := range []string{"instance move", "abort the move", provision.LayoutJournalPath(p.DataDir, host), "stands at its destination"} {
+ if r := res[host]; !strings.Contains(r.Reason, want) {
+ t.Errorf("reason %q does not say %q", r.Reason, want)
+ }
+ }
+ if r := res[host]; strings.Contains(r.Reason, "\u2014") {
+ t.Errorf("reason has an em dash: %q", r.Reason)
+ }
if j, found, err := ReadJournal(p.DataDir, host); err != nil || !found || j.Phase != PhaseDone {
t.Fatalf("journal: %+v %v %v", j, found, err)
}