hub v0.72.0 — R-70 + R-71c: offsite delivery-state detector, card, stuck event, R-39(a)-guarded self-heal restage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NKSN3gSg4TKVBBqkwW2djR
This commit is contained in:
2026-07-23 12:59:04 +02:00
parent c801cee647
commit 1133aade73
13 changed files with 1179 additions and 3 deletions
+112
View File
@@ -913,6 +913,77 @@ func (s *Store) SaveReport(customerID string, reportJSON []byte) error {
return err
}
// reportOffsitePresence is the minimal parse for "does this controller report carry an offsite
// status object" — the R-70 delivery-state signal. The report builder attaches `offsite` only when
// the box actually has an offbox target configured, so presence == applied-on-the-box.
type reportOffsitePresence struct {
Offsite json.RawMessage `json:"offsite"`
}
func reportHasOffsite(reportJSON string) bool {
var p reportOffsitePresence
if err := json.Unmarshal([]byte(reportJSON), &p); err != nil {
return false // unparseable report → no offsite evidence
}
return len(p.Offsite) > 0 && string(p.Offsite) != "null"
}
// LatestReportOffsitePresence reports whether the customer's most recent controller report exists
// and whether it carries an offsite status object (R-70 detector input).
func (s *Store) LatestReportOffsitePresence(customerID string) (found bool, receivedAt time.Time, hasOffsite bool, err error) {
var recv, reportJSON string
err = s.db.QueryRow(`SELECT received_at, report_json FROM reports WHERE customer_id = ? ORDER BY id DESC LIMIT 1`,
customerID).Scan(&recv, &reportJSON)
if err == sql.ErrNoRows {
return false, time.Time{}, false, nil
}
if err != nil {
return false, time.Time{}, false, err
}
return true, parseSQLiteTime(recv), reportHasOffsite(reportJSON), nil
}
// CountReportsOffsiteSince counts the customer's controller reports received strictly after `since`
// (UTC) and how many of them carry an offsite status object (R-70 detector input: "N consecutive
// reports since consume without offbox" == total>0 && withOffsite==0). Capped at 500 rows per call —
// far beyond any detector threshold; the cap only bounds memory.
func (s *Store) CountReportsOffsiteSince(customerID string, since time.Time) (total, withOffsite int, err error) {
rows, err := s.db.Query(`SELECT report_json FROM reports WHERE customer_id = ? AND received_at > ? ORDER BY id LIMIT 500`,
customerID, since.UTC().Format("2006-01-02 15:04:05"))
if err != nil {
return 0, 0, err
}
defer rows.Close()
for rows.Next() {
var reportJSON string
if err := rows.Scan(&reportJSON); err != nil {
return 0, 0, err
}
total++
if reportHasOffsite(reportJSON) {
withOffsite++
}
}
return total, withOffsite, rows.Err()
}
// LastEventAt returns the created_at of the most recent event of the given type for a customer
// (zero time when none). Durable across hub restarts — used as the cooldown/rate-limit source for
// hub-emitted detector events (R-70/R-71c: a repeating pattern must surface as repeating events on
// a bounded cadence, never as a silent retry loop OR a restart-reset flood).
func (s *Store) LastEventAt(customerID, eventType string) (time.Time, error) {
var createdAt string
err := s.db.QueryRow(`SELECT created_at FROM events WHERE customer_id = ? AND event_type = ? ORDER BY id DESC LIMIT 1`,
customerID, eventType).Scan(&createdAt)
if err == sql.ErrNoRows {
return time.Time{}, nil
}
if err != nil {
return time.Time{}, err
}
return parseSQLiteTime(createdAt), nil
}
// GetCustomers returns the latest report summary for each customer.
func (s *Store) GetCustomers() ([]CustomerSummary, error) {
rows, err := s.db.Query(`
@@ -1249,6 +1320,47 @@ func (s *Store) ConsumeOneTimeSecret(customerID string) (string, error) {
return value, nil
}
// OneTimeSecretInfo is the delivery-state metadata of a customer's one-time offsite secret —
// timestamps ONLY, the value column is deliberately never selected (R-70 detector input; the
// customer-scoped sibling of PBSDRHealStates' SecretUnconsumedFor).
type OneTimeSecretInfo struct {
CustomerID string
CreatedAt time.Time
ConsumedAt time.Time // zero = staged, not yet consumed
}
// GetOneTimeSecretInfo returns the timestamps of a customer's one-time offsite secret row, or nil
// when none exists. Never reads the value.
func (s *Store) GetOneTimeSecretInfo(customerID string) (*OneTimeSecretInfo, error) {
var createdAt string
var consumedAt sql.NullString
err := s.db.QueryRow(`SELECT created_at, consumed_at FROM one_time_secrets WHERE customer_id = ?`,
customerID).Scan(&createdAt, &consumedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
info := &OneTimeSecretInfo{CustomerID: customerID, CreatedAt: parseSQLiteTime(createdAt)}
if consumedAt.Valid {
info.ConsumedAt = parseSQLiteTime(consumedAt.String)
}
return info, nil
}
// SetOneTimeSecretTimesForTest back-dates a one-time secret's timestamps (SQLite datetime strings;
// consumedAt "" leaves it NULL) so grace/age behavior is testable without sleeping. TEST-ONLY —
// mirrors SetHostPBSSecretCreatedAtForTest.
func (s *Store) SetOneTimeSecretTimesForTest(customerID, createdAt, consumedAt string) error {
if consumedAt == "" {
_, err := s.db.Exec(`UPDATE one_time_secrets SET created_at = ?, consumed_at = NULL WHERE customer_id = ?`, createdAt, customerID)
return err
}
_, err := s.db.Exec(`UPDATE one_time_secrets SET created_at = ?, consumed_at = ? WHERE customer_id = ?`, createdAt, consumedAt, customerID)
return err
}
// ListCustomerConfigs returns all customer configurations ordered by ID.
func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
rows, err := s.db.Query(`