v0.203.0: the box collects what the hub staged for it (R-218 consume half) + R-220's message
gates / gates (push) Successful in 10s

R-218's declaration half shipped in v0.201.0 and works. Its consume half never
existed. Reconcile ran exactly twice per process — at start-up and when the
recovery screen drives it — and BOTH fire before the hub has anything staged,
because the hub stages in RESPONSE to the declaration those runs precede.

Measured on the R-201 re-walk: unlock reconcile 11:43:07, hub staged 11:44:57
saying 'next cycle', a full report cycle ran 11:55:46, still unconsumed at
12:06. A guest command line applied it in 18 seconds — everything correct except
the trigger.

Bridge.RetryIfDeclared re-runs the SAME reconcile on a 5-minute tick, driven from
the box's own published declaration (OffboxReportStatus().State) — the very
statement the hub acts on, so the two cannot disagree.

Poll, not an ACK flag, decided on the promise: the no-target message says 'amint
megvannak' (no deadline) and the card says 'within a day'. Five minutes is inside
both by a wide margin and needs no hub change.

It stops by construction — a healthy box does no work and logs nothing — and the
settle gate is deliberately kept via ReconcileWhenSettled.

The marker was investigated and left alone: applied_marker lives in the guest's
DataDir, which a rebuild destroys, so it cannot suppress a legitimate re-run.

R-220's customer half: the refusal no longer tells the customer to choose from a
list that may be empty. It names the rebuild, points at the Meghajtók page, and
promises no outcome.

Red-proofs: remove the retry -> credential uncollected (the dead end reproduced);
drop the stop condition -> a healthy box hammers the hub; call Reconcile instead
of ReconcileWhenSettled -> settle gate bypassed; restore the old sentence -> the
impossible action returns.

28 packages ok, vet clean, all controller gates OK.
This commit is contained in:
2026-08-06 12:56:12 +02:00
parent 66d80efb9f
commit 9dc26459ea
6 changed files with 290 additions and 1 deletions
@@ -341,3 +341,36 @@ func (b *Bridge) ReconcileWhenSettled(gateCtx context.Context) error {
defer cancel()
return b.Reconcile(ctx)
}
// ── R-218, THE CONSUME HALF ──────────────────────────────────────────────────────────────────────
//
// `Reconcile` was correct from the day it shipped and was simply never run again. It fires at
// start-up and once more when the recovery screen drives it (R-219) — and BOTH precede the moment the
// hub has anything staged, because the hub stages in RESPONSE to the declaration those runs come
// before. So the hub held a credential the box would never fetch.
//
// Measured on the R-201 re-walk, 2026-08-06: unlock reconcile 11:43:07 · hub staged 11:44:57 saying
// "the box re-consumes on its next cycle" · a full report cycle ran 11:55:46 · still unconsumed at
// 12:06. A guest command line moved it in 18 seconds — everything was fine except the trigger.
// NeedsCredentialFunc reports whether the box STILL declares it needs a transport credential. It is
// deliberately the box's own published declaration (`backup.OffboxReportStatus().State`) rather than a
// second predicate: the hub acts on that statement, so driving the retry from anything else would let
// the two disagree about whether a retry is wanted.
type NeedsCredentialFunc func() bool
// RetryIfDeclared is ONE tick of the consume half.
//
// It reconciles **only while the box declares a need**, which is what makes it stop: the instant a
// target exists the declaration goes false, this returns immediately, and a healthy box does no work
// and logs nothing. The settle gate is deliberately preserved — `ReconcileWhenSettled` waits for floor
// knowledge exactly as the start-up path does, because the day-0 race it guards is unchanged.
//
// Returns whether a reconcile was ATTEMPTED, so a caller (and a test) can tell "declined to run" from
// "ran and failed" without reading the log.
func (b *Bridge) RetryIfDeclared(ctx context.Context, declared NeedsCredentialFunc) (attempted bool, err error) {
if b == nil || declared == nil || !declared() {
return false, nil
}
return true, b.ReconcileWhenSettled(ctx)
}