code wiki / _hdl_build / nx_swapguard_beat.nx
nx_swapguard_beat.nx source
↩ module page · 130 lines · 6770 B
1// nx_swapguard_beat.nx -- SWAP-EXHAUSTION TRIPWIRE (2026-07-29, seq1256 lineage). Today's mgmt_api
2// and tools_api wedges were both preceded by HOURS of exhausted swap (12-196kB free) with no alarm
3// anywhere -- long-running seg-store consumers leak into swap (seq905/947 class) and then park in
4// uninterruptible sleep. This beat reads /proc/meminfo SwapFree every scheduler cycle and, on breach,
5// (a) appends an evidence line and (b) files a sev8 debt row THROUGH nx_debt whose desc is CONSTANT --
6// nx_debt add is content-idempotent (banked law 07-20), so hourly re-alarms dedupe to ONE standing row
7// and the board (which gates nx_ws_cycle work-emission) becomes the alarm surface. ALARM-ONLY by
8// design: auto-recycle needs the sanctioned restart paths (seq1256) and graduates only after this
9// tripwire proves calibrated.
10// scheduler contract: jobs.tsv fork+execs with NO args. argv[1]=floor_kb override (testing).
11// floor default 2097152 kB = 2GiB, DERIVED: swap total ~24GiB; every observed wedge sat at <200kB
12// for hours, fresh-boot free is >20GiB -- 2GiB (~8%) alarms early while never false-firing on a
13// healthy host (rule-11: promote to conf when field-tuned).
14// trend line: <epoch>\tSwapFree=<kb>\t<OK|ALARM>\n -> knowledge/status/swapguard_trend.jsonl
15// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 healthy, 1 alarm
16import "nx_sovjson_lib.nx"
17import "nx_syscalls.nx"
18import "nx_tool_run.nx"
19import "nx_gate_verdict.nx"
20
21const SG_FLOOR_KB: i64 = 2097152
22const SG_MODE: i64 = 420
23const SG_BUF: i64 = 8192
24const SG_HEALTHY_WITNESS_KB: i64 = 20000000 // ~19GiB: a comfortably-above-floor value for the gate's good-polarity witness
25
26func sg_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
27func sg_append(path: *u8, buf: *u8, n: i64) -> i64 {
28 let fd: i64 = sys_openat_append(path, SG_MODE)
29 if fd < 0 { return 0 - 1 }
30 sys_write(fd, buf, n)
31 sys_fsync(fd)
32 sys_close(fd)
33 return 0
34}
35// parse SwapFree kB out of /proc/meminfo; -1 if absent
36func sg_swapfree() -> i64 {
37 let b: *u8 = sys_mmap(SG_BUF)
38 let fd: i64 = sys_openat_rd("/proc/meminfo" as *u8)
39 if fd < 0 { return 0 - 1 }
40 var got: i64 = 0
41 var r: i64 = 1
42 while r > 0 { r = sys_read(fd, ((b as i64) + got) as *u8, SG_BUF - got); if r > 0 { got = got + r } if got >= SG_BUF { r = 0 } }
43 sys_close(fd)
44 let key: *u8 = "SwapFree:" as *u8
45 let kl: i64 = sj_vlen(key)
46 var i: i64 = 0
47 while i + kl < got {
48 var eq: i64 = 1
49 var k: i64 = 0
50 while k < kl { if b[i + k] != key[k] { eq = 0; k = kl } else { k = k + 1 } }
51 if eq == 1 {
52 var v: i64 = 0
53 var j: i64 = i + kl
54 var any: i64 = 0
55 var go: i64 = 1
56 while go == 1 {
57 go = 0
58 if j < got {
59 let c: i64 = b[j] as i64
60 if c == 32 { if any == 0 { j = j + 1; go = 1 } }
61 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; j = j + 1; go = 1 } }
62 }
63 }
64 if any == 1 { return v }
65 return 0 - 1
66 }
67 i = i + 1
68 }
69 return 0 - 1
70}
71// PURE breach decision -- one definition, main AND the gate call it (semantic consistency by construction).
72// Breach iff SwapFree unreadable (sf<0, treat as broken host) OR below the floor.
73func sg_breach(sf: i64, floor: i64) -> i64 {
74 if sf < 0 { return 1 }
75 if sf < floor { return 1 }
76 return 0
77}
78func sg_selftest() -> i64 {
79 let ctr: *i64 = gv_ctr()
80 gv_head("nx_swapguard_beat selftest -- breach threshold bite-proven both polarities" as *u8)
81 // floor = 2GiB (2097152 kB). Each cell fires on the BAD side, silent on the GOOD side.
82 gv_bite("T1 below-floor breaches, above-floor clears" as *u8, sg_breach(100, SG_FLOOR_KB), sg_breach(SG_HEALTHY_WITNESS_KB, SG_FLOOR_KB), ctr)
83 gv_bite("T2 unreadable-swap (sf<0) breaches, exact-floor clears" as *u8, sg_breach(0 - 1, SG_FLOOR_KB), sg_breach(SG_FLOOR_KB, SG_FLOOR_KB), ctr)
84 gv_bite("T3 zero-free breaches, floor+1 clears" as *u8, sg_breach(0, SG_FLOOR_KB), sg_breach(SG_FLOOR_KB + 1, SG_FLOOR_KB), ctr)
85 let rc: i64 = gv_verdict("NX-SWAPGUARD-GATE" as *u8, ctr, "swap-exhaustion breach decision: below-floor/unreadable fire, at-or-above clear" as *u8)
86 return rc
87}
88func main(argc: i64, argv: *i64) -> i64 {
89 if argc > 1 { if sj_lit_eq(argv[1] as *u8, 0, sj_vlen(argv[1] as *u8), "selftest" as *u8) == 1 { let g: i64 = sg_selftest(); sys_exit(g); return g } }
90 var sgfloor: i64 = SG_FLOOR_KB
91 if argc > 1 { sgfloor = sj_atoi_z(argv[1] as *u8) }
92 let sf: i64 = sg_swapfree()
93 let now: i64 = sys_now_realtime_sec()
94 let ln: *u8 = sys_mmap(SG_BUF)
95 var o: i64 = sj_catn(ln, 0, now)
96 ln[o] = 9 as u8
97 o = o + 1
98 o = sj_cat(ln, o, "SwapFree=" as *u8)
99 o = sj_catn(ln, o, sf)
100 ln[o] = 9 as u8
101 o = o + 1
102 let sgalarm: i64 = sg_breach(sf, sgfloor)
103 if sgalarm == 1 { o = sj_cat(ln, o, "ALARM" as *u8) } else { o = sj_cat(ln, o, "OK" as *u8) }
104 ln[o] = 10 as u8
105 o = o + 1
106 sg_append("knowledge/status/swapguard_trend.jsonl" as *u8, ln, o)
107 if sgalarm == 0 { sg_puts("SWAPGUARD OK\n" as *u8); sys_exit(0); return 0 }
108 // file the STANDING alarm row through the sovereign board verb; CONSTANT desc = content-idempotent
109 let elf: *u8 = "/volume1/homes/elderwesto/nishihost/nx_debt.elf" as *u8
110 // CONSTANT desc BY DESIGN (2026-07-29 seq1271 root fix): nx_debt dedup now matches OPEN rows
111 // only, so while the alarm row stands, hourly re-alarms dedupe to it; once eaten (swap
112 // recovered), the next real breach re-files cleanly. The day-bucket workaround is deleted.
113 let av: *i64 = sys_mmap(64) as *i64
114 av[0] = elf as i64
115 av[1] = ("add" as *u8) as i64
116 av[2] = ("8" as *u8) as i64
117 av[3] = ("platform" as *u8) as i64
118 av[4] = ("SWAP-EXHAUSTION TRIPWIRE FIRING (nx_swapguard_beat): host SwapFree is below the derived 2GiB floor. The seq905/947 seg-store leak class is filling swap; long-running daemons (tools_api/mgmt_api pattern, wedges of 2026-07-29) park in uninterruptible sleep and the control plane wedges NEXT. Recycle the leaky daemon (holder-inode kill recipe, census memory + ctnr_recover.sh) and advance the ss_close consumer migration. Eat when swap recovers; re-files on the next breach." as *u8) as i64
119 av[5] = 0
120 let out: *u8 = sys_mmap(SG_BUF)
121 let ol: *i64 = sys_mmap(16) as *i64
122 let rc: i64 = tr_run_capture(elf, av, out, SG_BUF, ol)
123 sg_puts("SWAPGUARD ALARM filed=" as *u8)
124 let m: *u8 = sys_mmap(64)
125 var mo: i64 = sj_catn(m, 0, rc)
126 m[mo] = 10 as u8
127 sys_write(1, m, mo + 1)
128 sys_exit(1)
129 return 1
130}