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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user