hub v0.114.0: self-bind auto-send while a customer waits for a box (R-509); node_* bypass the quiet hour (ruling 2026-09-15); PBS re-issue adopts an endpoint token (R-511); controller supervisor events (R-523); event registers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-15 10:05:41 +02:00
parent a028a9a7f5
commit 07959e61b5
16 changed files with 693 additions and 9 deletions
+41 -2
View File
@@ -396,6 +396,33 @@ func cooldownStackSuffix(eventType, detailsJSON string) string {
return ":" + d.StackName
}
// nodeLivenessEvents skip the 1-hour operator cooldown (OPERATOR RULING 2026-09-15, decision A;
// 08-alarm-ladder.md §5). BIGNIGHT F9: the box was dead for 33 minutes and the `node_stale` mail was
// suppressed because F8's `node_stale` had used the hour 39 minutes earlier; the `node_recovered`
// mail was suppressed the same way. "The box is down" must not wait out a quiet hour. A 5-minute
// dedupe stays, so a flapping link cannot mail every sweep. The key is unchanged (customer:type), and
// a customer has one box, so the dedupe is per host. host_* (agent-plane) siblings are NOT in the
// ruling and keep the hour.
var nodeLivenessEvents = map[string]bool{
"node_stale": true,
"node_down": true,
"node_recovered": true,
}
const (
operatorCooldown = 1 * time.Hour
nodeLivenessDedupeWindow = 5 * time.Minute
)
// operatorCooldownFor returns the operator-mail cooldown for an event type. Pinned by
// TestOperatorCooldown_NodeLivenessBypassesQuietHour.
func operatorCooldownFor(eventType string) time.Duration {
if nodeLivenessEvents[eventType] {
return nodeLivenessDedupeWindow
}
return operatorCooldown
}
func (d *Dispatcher) processOperator(customerID, eventType, severity, message, detailsJSON, source string) {
if !d.operatorOn || d.operatorEmail == "" {
return
@@ -406,8 +433,9 @@ func (d *Dispatcher) processOperator(customerID, eventType, severity, message, d
cooldownKey := customerID + ":" + eventType +
cooldownTierSuffix(detailsJSON) + cooldownRunSuffix(detailsJSON) +
cooldownStackSuffix(eventType, detailsJSON)
window := operatorCooldownFor(eventType)
d.mu.Lock()
if last, ok := d.opCooldowns[cooldownKey]; ok && time.Since(last) < 1*time.Hour {
if last, ok := d.opCooldowns[cooldownKey]; ok && time.Since(last) < window {
d.mu.Unlock()
// R-182: RECORD THE SUPPRESSION. This used to be a bare `return` — the event was dropped
// before any LogNotification, so a cooldown drop and an event that never happened were
@@ -423,7 +451,7 @@ func (d *Dispatcher) processOperator(customerID, eventType, severity, message, d
// applies to EVERY operator event, not only the one that exposed it. It makes the drop
// visible; it deliberately does NOT change the cooldown's duration or semantics.
if err := d.store.LogNotification(customerID, eventType, severity, message,
"suppressed", "operator cooldown 1h, key="+cooldownKey, "operator"); err != nil {
"suppressed", "operator cooldown "+window.String()+", key="+cooldownKey, "operator"); err != nil {
d.logger.Printf("[WARN] Failed to record suppressed operator notification for %s/%s: %v",
customerID, eventType, err)
}
@@ -554,6 +582,17 @@ var operatorOnlyEvents = map[string]bool{
// mints the type — an operator-tier type absent from this register reaches customers as raw
// English (the v0.78.0 defect recorded above).
"escrow_blob_served": true,
// R-523 (v0.114.0). The agent restarted a dead in-guest controller, or gave up after repeated
// restarts. Operator-grade (host ids, vmids, raw docker states) and not actionable by a household
// — the customer's side of this is the dashboard coming back. Registered in the same commit that
// mints the types.
"controller_restarted_by_agent": true,
"controller_crashloop": true,
// R-518 / R-514 (v0.114.0, controller v0.243.0). A whole-guest tier skipped for absent storage is a
// provisioning fact the household cannot act on; an OOM-killed app process carries raw container
// names — the household's side is the dashboard tag. Registered in the same commit.
"backup_tier_skipped": true,
"app_oom": true,
}
// IsOperatorOnly reports whether an event type is barred from customer dispatch. Exported so the