R-330: the agent sends SMART 187/188/199 (raw; omitted when unknown)
gates / gates (push) Successful in 53s
gates / gates (push) Successful in 53s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
## Unreleased (2026-10-06 night, later) — after a restart the agent remembers the last backup per tier (R-894)
|
## Unreleased (2026-10-06 night, later) — after a restart the agent remembers the last backup per tier (R-894); three more SMART counters on the wire (R-330)
|
||||||
|
|
||||||
Ships with the memory-kill check below as v0.150.0, AFTER the 2026-10-07 night read-back. Nothing delivered tonight.
|
Ships with the memory-kill check below as v0.150.0, AFTER the 2026-10-07 night read-back. Nothing delivered tonight.
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ Ships with the memory-kill check below as v0.150.0, AFTER the 2026-10-07 night r
|
|||||||
- `internal/localapi` `handleBackupDue`: when the tier's storage CANNOT be read, the saved copy stands in for the in-memory record. A fresh copy → not due („… (storage unreadable — age from the last success saved on disk)"); a copy older than the cadence → DUE; no copy → the old answer (DUE, age unknown). A storage that answers stays the ground truth: an archive absent there is due even when the file remembers one.
|
- `internal/localapi` `handleBackupDue`: when the tier's storage CANNOT be read, the saved copy stands in for the in-memory record. A fresh copy → not due („… (storage unreadable — age from the last success saved on disk)"); a copy older than the cadence → DUE; no copy → the old answer (DUE, age unknown). A storage that answers stays the ground truth: an archive absent there is due even when the file remembers one.
|
||||||
- Wired in `buildLocalAPIServer` (`LastKnownBackups`); the local API's backup job saves each success.
|
- Wired in `buildLocalAPIServer` (`LastKnownBackups`); the local API's backup job saves each success.
|
||||||
- Tests: `TestBackupDue_R894_*` (restart = a new server and a new state from the same file; fresh / old / none / storage answers / failed backup not saved), `TestBackupSuccessState_*`, `TestR894_LastKnownBackupsIsWiredIntoTheDaemon` (AST). Four red-proofs observed (`felhom.eu/documentation/audits/night-burndown-2026-10-06/s4/`).
|
- Tests: `TestBackupDue_R894_*` (restart = a new server and a new state from the same file; fresh / old / none / storage answers / failed backup not saved), `TestBackupSuccessState_*`, `TestR894_LastKnownBackupsIsWiredIntoTheDaemon` (AST). Four red-proofs observed (`felhom.eu/documentation/audits/night-burndown-2026-10-06/s4/`).
|
||||||
|
- **R-330 (disk health Phase 2, the wire only):** the SMART summary carries three more SATA raw counters — `reported_uncorrect` (187), `command_timeout` (188, carried as the vendor reports it; some pack several counters), `udma_crc_errors` (199). Pointer + omitempty: an attribute the drive does not report is OMITTED (unknown), never 0. No verdict reads them yet. Tests `TestParseSMART_R330_*` (two red-proofs, `felhom.eu/documentation/audits/night-burndown-2026-10-06/r330/`).
|
||||||
|
|
||||||
## Unreleased (2026-10-06 night) — the Docker step proves the engine reports a memory kill (`09` §3 decision 157, R-528)
|
## Unreleased (2026-10-06 night) — the Docker step proves the engine reports a memory kill (`09` §3 decision 157, R-528)
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,17 @@ type SmartSummary struct {
|
|||||||
ReallocatedSectors *int `json:"reallocated_sectors"`
|
ReallocatedSectors *int `json:"reallocated_sectors"`
|
||||||
PendingSectors *int `json:"pending_sectors"`
|
PendingSectors *int `json:"pending_sectors"`
|
||||||
OfflineUncorrectable *int `json:"offline_uncorrectable"`
|
OfflineUncorrectable *int `json:"offline_uncorrectable"`
|
||||||
|
// R-330 (disk health Phase 2): three more SATA raw counters. omitempty + pointer: absent (an
|
||||||
|
// older agent, an NVMe/USB device, or a drive that does not report the attribute) is OMITTED —
|
||||||
|
// unknown, never a zero (S-39). Wire only: no verdict reads them yet.
|
||||||
|
// 187 Reported_Uncorrect — the failing drive's most telling counter (normalized 1 vs thresh 0,
|
||||||
|
// raw 1001) while SMART still said PASSED.
|
||||||
|
// 188 Command_Timeout — some vendors PACK several counters into the 48-bit raw value, so the
|
||||||
|
// number is carried as reported and must not be compared across vendors.
|
||||||
|
// 199 UDMA_CRC_Error_Count — cabling / link errors, not the medium.
|
||||||
|
ReportedUncorrect *int64 `json:"reported_uncorrect,omitempty"`
|
||||||
|
CommandTimeout *int64 `json:"command_timeout,omitempty"`
|
||||||
|
UDMACRCErrors *int64 `json:"udma_crc_errors,omitempty"`
|
||||||
|
|
||||||
// NVMe attributes.
|
// NVMe attributes.
|
||||||
CriticalWarning *int `json:"critical_warning"`
|
CriticalWarning *int `json:"critical_warning"`
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-330 (disk health Phase 2) — attributes 187, 188 and 199 ride the wire.
|
||||||
|
//
|
||||||
|
// The failing drive of 2026-08-14 carried 187 Reported_Uncorrect at raw 1001 while SMART said PASSED;
|
||||||
|
// none of the three reached the controller. The values are carried as RAW counters; an absent
|
||||||
|
// attribute is OMITTED from the JSON (unknown), never sent as 0 (S-39).
|
||||||
|
|
||||||
|
// The 2026-08-14 shape: PASSED, 187 raw 1001, plus 188/199 and the existing three.
|
||||||
|
const r330SATA = `{"smart_status":{"passed":true},"ata_smart_attributes":{"table":[
|
||||||
|
{"id":5,"raw":{"value":0}},
|
||||||
|
{"id":187,"raw":{"value":1001}},
|
||||||
|
{"id":188,"raw":{"value":4295032833}},
|
||||||
|
{"id":197,"raw":{"value":8}},
|
||||||
|
{"id":198,"raw":{"value":8}},
|
||||||
|
{"id":199,"raw":{"value":3}}]}}`
|
||||||
|
|
||||||
|
// COMPANION RED-PROOF (observed): delete the three R-330 cases in parseSMART → this fails with
|
||||||
|
// "187 Reported_Uncorrect must be carried (raw 1001); got <nil>". Restored.
|
||||||
|
func TestParseSMART_R330_CarriesTheThreeCounters(t *testing.T) {
|
||||||
|
s := parseSMART([]byte(r330SATA))
|
||||||
|
if s.ReportedUncorrect == nil || *s.ReportedUncorrect != 1001 {
|
||||||
|
t.Fatalf("187 Reported_Uncorrect must be carried (raw 1001); got %v", s.ReportedUncorrect)
|
||||||
|
}
|
||||||
|
// 188's raw value is vendor-packed on some drives (this one is 0x100010001): carried as reported, not truncated.
|
||||||
|
if s.CommandTimeout == nil || *s.CommandTimeout != 4295032833 {
|
||||||
|
t.Fatalf("188 Command_Timeout must be carried as the full raw value; got %v", s.CommandTimeout)
|
||||||
|
}
|
||||||
|
if s.UDMACRCErrors == nil || *s.UDMACRCErrors != 3 {
|
||||||
|
t.Fatalf("199 UDMA_CRC_Error_Count must be carried (raw 3); got %v", s.UDMACRCErrors)
|
||||||
|
}
|
||||||
|
if s.PendingSectors == nil || *s.PendingSectors != 8 {
|
||||||
|
t.Fatalf("the existing counters must be unchanged; pending=%v", s.PendingSectors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A drive (or an NVMe device) that does not report the attributes leaves them nil, and the JSON
|
||||||
|
// OMITS the keys — the receiver reads "unknown", never a measured zero.
|
||||||
|
//
|
||||||
|
// COMPANION RED-PROOF (observed): drop `,omitempty` from the three tags in hub.SmartSummary → this
|
||||||
|
// fails with "an unreported attribute must be omitted, not sent: … reported_uncorrect …". Restored.
|
||||||
|
func TestParseSMART_R330_AbsentIsOmittedNotZero(t *testing.T) {
|
||||||
|
for name, raw := range map[string]string{
|
||||||
|
"sata without the three": `{"smart_status":{"passed":true},"ata_smart_attributes":{"table":[{"id":5,"raw":{"value":0}}]}}`,
|
||||||
|
"nvme": `{"smart_status":{"passed":true},"nvme_smart_health_information_log":{"critical_warning":0,"media_errors":0,"percentage_used":3}}`,
|
||||||
|
} {
|
||||||
|
s := parseSMART([]byte(raw))
|
||||||
|
if s.ReportedUncorrect != nil || s.CommandTimeout != nil || s.UDMACRCErrors != nil {
|
||||||
|
t.Fatalf("%s: unreported attributes must stay nil; got %v %v %v", name, s.ReportedUncorrect, s.CommandTimeout, s.UDMACRCErrors)
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(s)
|
||||||
|
for _, k := range []string{"reported_uncorrect", "command_timeout", "udma_crc_errors"} {
|
||||||
|
if strings.Contains(string(b), `"`+k+`"`) {
|
||||||
|
t.Fatalf("%s: an unreported attribute must be omitted, not sent: %s", name, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,9 @@ const (
|
|||||||
ataReallocatedSectorCt = 5
|
ataReallocatedSectorCt = 5
|
||||||
ataCurrentPending = 197
|
ataCurrentPending = 197
|
||||||
ataOfflineUncorrect = 198
|
ataOfflineUncorrect = 198
|
||||||
|
ataReportedUncorrect = 187 // R-330
|
||||||
|
ataCommandTimeout = 188 // R-330
|
||||||
|
ataUDMACRCErrorCount = 199 // R-330
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseSMART maps smartctl JSON to a hub.SmartSummary, handling SATA + NVMe and degrading
|
// parseSMART maps smartctl JSON to a hub.SmartSummary, handling SATA + NVMe and degrading
|
||||||
@@ -85,6 +88,12 @@ func parseSMART(raw []byte) hub.SmartSummary {
|
|||||||
s.PendingSectors = intPtr(int(a.Raw.Value))
|
s.PendingSectors = intPtr(int(a.Raw.Value))
|
||||||
case ataOfflineUncorrect:
|
case ataOfflineUncorrect:
|
||||||
s.OfflineUncorrectable = intPtr(int(a.Raw.Value))
|
s.OfflineUncorrectable = intPtr(int(a.Raw.Value))
|
||||||
|
case ataReportedUncorrect:
|
||||||
|
s.ReportedUncorrect = int64Ptr(a.Raw.Value)
|
||||||
|
case ataCommandTimeout:
|
||||||
|
s.CommandTimeout = int64Ptr(a.Raw.Value)
|
||||||
|
case ataUDMACRCErrorCount:
|
||||||
|
s.UDMACRCErrors = int64Ptr(a.Raw.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,3 +151,5 @@ func parseThinPoolMetadata(raw []byte) (float64, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func intPtr(v int) *int { return &v }
|
func intPtr(v int) *int { return &v }
|
||||||
|
|
||||||
|
func int64Ptr(v int64) *int64 { return &v }
|
||||||
|
|||||||
Reference in New Issue
Block a user