Read the vault's structured move-in refusal and take a value it holds another of as superseded, so the move is not asked again for ever (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-host@ee71ba4dea81
mesh/delivery-group group fix/sweep-2026-10-30 merged: every member is merged
mesh/check checking b9c24a7b42cb on 737bce9c9448 | novox/mesh-host#179 in group fix/sweep-2026-10-30

mesh-vault (mesh-catalog f3b13de2) answers a move it refuses with `refused: {reason, held}` and `why`, and
no read-back. The engine read only the read-back, so every such row failed "answered with no read-back"
and the controller asked it again on every pass.

Quick fix: treat any answer without a read-back as done. Thorough fix (chosen): read `refused`. For
holds-another-value the vault's value is the live one: the move answers done, superseded, at the held
generation with the held record in its words, so the controller records the row and stops asking; the
journal says it once per secret. Any other reason fails in the vault's own words.
This commit is contained in:
2026-10-30 18:36:38 +01:00
parent 744bfec4a0
commit b9c24a7b42
4 changed files with 102 additions and 11 deletions
+11 -1
View File
@@ -54,6 +54,9 @@ type MoveSecretAnswer struct {
Secret string `json:"secret,omitempty"`
Generation int `json:"generation,omitempty"`
Unchanged bool `json:"unchanged,omitempty"`
// Superseded is set when the vault holds another value for the name, which is the live one: the row is done and
// is not asked again; Generation is the held value's, and Said its record in words.
Superseded bool `json:"superseded,omitempty"`
}
// MoveSecret is one move asked and believed, as the handler is given it: the row, this machine, and who asked.
@@ -70,6 +73,9 @@ type MovedSecret struct {
Secret string
Generation int
Unchanged bool
// Superseded says, in words, that the vault holds another value for the name, which is the live one: nothing was
// moved, and the move is done (the vault's record of the held value, its generation in Generation).
Superseded string
}
// MoveSecretFunc moves one value into the vault: what it kept, or why not.
@@ -133,7 +139,11 @@ func AnswerMoveSecret(check HandOverCheck, handle MoveSecretFunc, body []byte) M
if moved.Unchanged {
said = fmt.Sprintf("the vault held %s already, the same value (generation %d)", moved.Secret, moved.Generation)
}
return MoveSecretAnswer{Said: said, Secret: moved.Secret, Generation: moved.Generation, Unchanged: moved.Unchanged}
if moved.Superseded != "" {
said = moved.Superseded
}
return MoveSecretAnswer{Said: said, Secret: moved.Secret, Generation: moved.Generation, Unchanged: moved.Unchanged,
Superseded: moved.Superseded != ""}
}
// MoveSecretReply is what a move request is answered with, and whether to answer, as HandOverReply.
+3 -2
View File
@@ -28,8 +28,9 @@ func TestTheMovesAskKeepsItsFieldNamesAndContext(t *testing.T) {
if MoveSecretContext != "novox-mesh move-secret v1\n" {
t.Fatalf("the signing context is %q", MoveSecretContext)
}
answer, _ := json.Marshal(MoveSecretAnswer{Said: "s", Refused: "r", Secret: "own/a/b/c", Generation: 1, Unchanged: true})
if got := keysOf(t, answer); got != "generation refused said secret unchanged" {
answer, _ := json.Marshal(MoveSecretAnswer{Said: "s", Refused: "r", Secret: "own/a/b/c", Generation: 1, Unchanged: true,
Superseded: true})
if got := keysOf(t, answer); got != "generation refused said secret superseded unchanged" {
t.Fatalf("the answer's fields are %q", got)
}
}