feat(report): Direction-2 immediate-sync wait channel client (v0.140.0)

report.Waiter holds a hanging GET against hub /api/v1/wait?gen=N (same hub
URL+key as the pusher). On a generation change it fires the v0.139.0
report.Trigger — nothing else; the report ACK delivers everything through the
unchanged machinery. No overall client timeout (held GET); per-request ctx
bounds a dead connection. First-observation records-not-fires; same-gen
timeout fires nothing; errors (incl. 404 from a pre-v0.58.0 hub) back off
5s->5min while the 15-min cycle reconciles. Wired beside the trigger under the
same hubPusher!=nil && Hub.Enabled gate.

Red-proof: disable the baseline branch -> first observation fires
(TestWaiter_FirstObservationRecordsNoFire), run-fail-reverted.

Copy soften: backups_remote/escrow "néhány másodperc, legfeljebb 15 perc".
Pairs with hub v0.58.0. Grounding:
felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md
This commit is contained in:
2026-07-16 20:55:30 +02:00
parent f665bbed45
commit 2dd05670ae
7 changed files with 415 additions and 2 deletions
+31
View File
@@ -1,5 +1,36 @@
## Changelog
### v0.140.0 — Direction-2 immediate-sync: hub→box wait channel client (2026-07-16)
The other half of the immediacy arc (Direction 1 = v0.139.0 box→hub trigger). An operator action on
the hub now reaches the box in **seconds** instead of on the next ~15-min cycle. Pairs with hub
v0.58.0 (the `GET /api/v1/wait` endpoint + the in-memory operator-intent notifier). Grounding:
`felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md` (option b).
- **`internal/report/waiter.go` (new) `report.Waiter`:** holds a hanging authenticated GET against
the hub's `/api/v1/wait?gen=N` (reusing the SAME hub URL + key as the pusher — no new config
keys). Its own `http.Client` has **no overall Timeout** (a held GET must stay open for the hub's
~240 s hold) with sane connect/TLS/`ResponseHeaderTimeout` deadlines; a per-request context bounds
a black-holed connection. On a completion whose generation **differs** from the last seen, it fires
the v0.139.0 `report.Trigger` — and NOTHING else; the fired report's ACK delivers config/escrow/
claim/floor through the UNCHANGED machinery (this adds zero delivery logic). Behaviors:
- **First observation records, never fires** (the startup report already covered current state) —
prevents a spurious echo report on every process start / config-refresh restart. Red-proof:
disable the baseline branch → `TestWaiter_FirstObservationRecordsNoFire` fires 1 (run-fail-reverted).
- **Same-generation timeout fires nothing** (the hub's hold elapsed) — not interval-shortening.
- Heartbeat newlines tolerated; the body is read only for its `{"gen":N}` line (contentless wake).
- Any error — transport, a **404 from a hub that predates the endpoint**, or a malformed body —
backs off exponentially (5 s → 5 min, reset on success) with ONE WARN per state change, and the
15-min cycle keeps reconciling. Exits promptly on context cancel (even mid-hold).
- **`cmd/controller/main.go`:** the Waiter is constructed + started right beside the Direction-1
trigger, gated on the SAME `hubPusher != nil && cfg.Hub.Enabled` condition (strict no-op when hub
reporting is off). One INFO line on start.
- **Copy soften (Viktor-approved):** `backups_remote.html` + `backups_escrow.html` — "ez általában
néhány **másodperc**, legfeljebb 15 perc" (was "néhány perc"). The 15-min bound stays — it is the
honest worst case when both the wait and the immediate push fail. Escrow grace window unchanged.
- **Coupling (soft):** immediacy needs hub ≥ v0.58.0; against an older hub the wait 404s and the box
degrades cleanly to the 15-min cycle. No agent coupling, no `MinAgent`.
### v0.139.0 — immediate out-of-cycle hub report on user actions (Direction 1) (2026-07-16)
Viktor's ruling: a user action with hub-side effects must round-trip in seconds, not minutes. One