From b3b13f2ae1a705f2772cedf3975e5605344c4901 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:06:39 +0100 Subject: [PATCH] =?UTF-8?q?G=20=C2=A7560:=20a=20cancelled=20recovery=20act?= =?UTF-8?q?=20holds=20the=20move=20with=20the=20hand=20act's=20why?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G A-117, §560 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- internal/agent/kept.go | 8 +++++++- internal/agent/kept_test.go | 8 ++++++-- internal/link/recovery.go | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/agent/kept.go b/internal/agent/kept.go index 056728fa..19bcebe9 100644 --- a/internal/agent/kept.go +++ b/internal/agent/kept.go @@ -26,6 +26,8 @@ type Ending struct { Outcome string Error string Snapshot string + // Why is the hand act's words for a cancel (design 47 G A-117, §560). + Why string } // RecoveryWaitMinutes is the default of the manifest's config key `recovery-wait-minutes`, the bound of the wait for an @@ -191,7 +193,11 @@ func (a *Agent) askAndWait(ctx context.Context, h Have, kind string, k KeptMove, case ev.Error == "stopped": act, wait.Act = "", "" case ev.Error == "cancelled": - return ev, &recoveryHeld{Words: "cancelled"} + words := "cancelled" + if ev.Why != "" { + words += ": " + ev.Why + } + return ev, &recoveryHeld{Words: words} default: words := ev.Error if words == "" { diff --git a/internal/agent/kept_test.go b/internal/agent/kept_test.go index 9578891b..31b0272c 100644 --- a/internal/agent/kept_test.go +++ b/internal/agent/kept_test.go @@ -143,7 +143,11 @@ func testAMoveAcrossAFormChange(t *testing.T) { fp := func(c string) string { return fpOf(c) } done := func(act string) Ending { return Ending{Act: act, Outcome: meshrecovery.EndedEventOutcomeDone} } failed := func(act, why string) Ending { - return Ending{Act: act, Outcome: meshrecovery.EndedEventOutcomeFailed, Error: why} + e := Ending{Act: act, Outcome: meshrecovery.EndedEventOutcomeFailed, Error: why} + if why == "cancelled" { + e.Why = "the operator stopped it" // the hand act's words (design 47 G A-117) + } + return e } applied := func(b *bed) bool { return len(b.appliedList()) == 2 } heldHave := func(b *bed) bool { return b.haves.last("db").Phase == "held" } @@ -174,7 +178,7 @@ func testAMoveAcrossAFormChange(t *testing.T) { }) for _, row := range []struct{ error, held string }{ - {"cancelled", "cancelled"}, + {"cancelled", "cancelled: the operator stopped it"}, {"cap", "cap"}, {"restore point damaged", "restore point damaged"}, {"disk full", "disk full"}, diff --git a/internal/link/recovery.go b/internal/link/recovery.go index 63be88bb..8bfde8f0 100644 --- a/internal/link/recovery.go +++ b/internal/link/recovery.go @@ -140,6 +140,8 @@ type RecoveryEnding struct { Outcome string `json:"outcome"` Error string `json:"error,omitempty"` Snapshot string `json:"snapshot,omitempty"` + // Why is the hand act's words for a cancel (design 47 G A-117, §560). + Why string `json:"why,omitempty"` } // HearRecoveryEnding follows the events `ended` (kind "ended") or `restored` (kind "restored") of one module's kept data: the last