hub v0.122.0: app_hold_no_whole_copy — allow-listed, operator-only, per-app cooldown (R-659)
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
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:
@@ -1,3 +1,17 @@
|
||||
## v0.122.0 — `app_hold_no_whole_copy`: a stranded held app reaches support (2026-09-24, R-659)
|
||||
|
||||
Controller v0.268.0 sends `app_hold_no_whole_copy` (severity `critical`) when an update AND its
|
||||
automatic undo failed and the box holds no copy that brings the app back together with its files
|
||||
(operator ruling 2026-09-24, `09` §3 decision 25, option A). Details: `app`, `stack_name`, `from`, `to`,
|
||||
`at`, `copies_seen`, `undo_state`. The household still gets its `app_update_held` mail, whose sentence
|
||||
now says that no such copy exists and that support is informed.
|
||||
|
||||
- **Allow-listed AND operator-only, in the same commit** (`allowedEventTypes` + `operatorOnlyEvents`).
|
||||
Pinned together by `TestAppHoldNoWholeCopyIsAllowlistedAndOperatorOnly` (red-proof: dropped from
|
||||
`operatorOnlyEvents` → the test fails at "must be operator-only").
|
||||
- **Per-app operator cooldown:** joined R-389's `perAppCooldownEvents` (the fence test widened on
|
||||
purpose, with its reason) — two apps stranded on one night are two alarms.
|
||||
|
||||
## v0.121.0 — `app_oom_storm`: a repeating OOM problem reaches the operator louder (2026-09-23, R-636)
|
||||
|
||||
Controller v0.265.0 sends `app_oom_storm` (severity `error`) once per container run when the kernel's
|
||||
|
||||
@@ -80,3 +80,15 @@ func TestAppOOMStormIsAllowlistedAndOperatorOnly(t *testing.T) {
|
||||
t.Fatalf("%s must be operator-only — container names and memory figures are not a household's business", et)
|
||||
}
|
||||
}
|
||||
|
||||
// R-659 (v0.122.0) — app_hold_no_whole_copy is allow-listed AND operator-only, pinned together.
|
||||
// RED-PROOF (REPORT.md): drop it from operatorOnlyEvents → "must be operator-only".
|
||||
func TestAppHoldNoWholeCopyIsAllowlistedAndOperatorOnly(t *testing.T) {
|
||||
et := "app_hold_no_whole_copy"
|
||||
if !allowedEventTypes[et] {
|
||||
t.Fatalf("%s must be in allowedEventTypes — the controller's push would 400", et)
|
||||
}
|
||||
if !notify.IsOperatorOnly(et) {
|
||||
t.Fatalf("%s must be operator-only — the household is told by app_update_held; this carries support's detail", et)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2143,6 +2143,10 @@ var allowedEventTypes = map[string]bool{
|
||||
// R-636 (hub v0.121.0, controller v0.265.0): the same container run OOM-killed 20+ times in 30 min.
|
||||
// Operator-only (notify.operatorOnlyEvents) — registered in BOTH in the same commit.
|
||||
"app_oom_storm": true,
|
||||
// R-659 (hub v0.122.0, controller v0.268.0): an app held after a failed update and a failed undo
|
||||
// with NO copy on its box that brings it back whole — support must act. Operator-only
|
||||
// (notify.operatorOnlyEvents) — registered in BOTH in the same commit.
|
||||
"app_hold_no_whole_copy": true,
|
||||
// Special
|
||||
"test": true,
|
||||
}
|
||||
|
||||
@@ -395,6 +395,9 @@ var perAppCooldownEvents = map[string]bool{
|
||||
// v0.121.0 (R-636): a storm is one app's event with no digest behind it — two apps storming on one
|
||||
// night are two alarms. (The controller already sends it at most once per container run.)
|
||||
"app_oom_storm": true,
|
||||
// v0.122.0 (R-659): a held app with no whole copy on its box is one app's event with no digest —
|
||||
// two apps stranded on one night are two alarms, and the second must not be swallowed.
|
||||
"app_hold_no_whole_copy": true,
|
||||
}
|
||||
|
||||
// perAppCustomerCooldownEvents is the CUSTOMER-leg sibling of perAppCooldownEvents (v0.120.0). The
|
||||
@@ -659,6 +662,12 @@ var operatorOnlyEvents = map[string]bool{
|
||||
// OOM-killed 20+ times in 30 minutes. Same audience, same reason — raw container names and memory
|
||||
// figures; the household's side is the dashboard. Registered in the same commit that mints it.
|
||||
"app_oom_storm": true,
|
||||
// R-659 (v0.122.0, controller v0.268.0; operator ruling 2026-09-24, `09` §3 decision 25). A held app
|
||||
// whose box holds no copy that brings it back WHOLE. The household is told by its own
|
||||
// `app_update_held` mail, in its language, that support is informed; THIS is that information —
|
||||
// operator-grade (the copies seen, per tier, with dates), and the act it calls for is support's.
|
||||
// Registered in the same commit that mints it.
|
||||
"app_hold_no_whole_copy": true,
|
||||
}
|
||||
|
||||
// IsOperatorOnly reports whether an event type is barred from customer dispatch. Exported so the
|
||||
|
||||
@@ -69,7 +69,8 @@ func TestR389_StackSuffixIsAllowListedAndFailSoft(t *testing.T) {
|
||||
// two alarms. The backup family stays coarse, as the fence says.
|
||||
func TestR389_TheAllowListHasExactlyOneMember(t *testing.T) {
|
||||
// v0.121.0 widened it again, on purpose (R-636): an OOM storm is one app's event with no digest.
|
||||
want := []string{"app_oom_storm", "app_start_failed", "app_update_held", "app_update_undone"}
|
||||
// v0.122.0 once more (R-659): a stranded held app is one app's event with no digest.
|
||||
want := []string{"app_hold_no_whole_copy", "app_oom_storm", "app_start_failed", "app_update_held", "app_update_undone"}
|
||||
ok := len(perAppCooldownEvents) == len(want)
|
||||
for _, w := range want {
|
||||
ok = ok && perAppCooldownEvents[w]
|
||||
@@ -79,7 +80,7 @@ func TestR389_TheAllowListHasExactlyOneMember(t *testing.T) {
|
||||
for k := range perAppCooldownEvents {
|
||||
got = append(got, k)
|
||||
}
|
||||
t.Fatalf("perAppCooldownEvents = %v, want exactly [app_oom_storm app_start_failed app_update_held app_update_undone]. Adding a member is the "+
|
||||
t.Fatalf("perAppCooldownEvents = %v, want exactly [app_hold_no_whole_copy app_oom_storm app_start_failed app_update_held app_update_undone]. Adding a member is the "+
|
||||
"fenced act: the backup family's cooldown is coarse ON PURPOSE (R-97a, R-182) so one full "+
|
||||
"disk sends one digest, not one mail per app. Read the fence before widening this.", got)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user