code wiki / _hdl_build / nx_procchurn.nx
nx_procchurn.nx source
↩ module page · 343 lines · 20244 B
1// nx_procchurn.nx -- the PROCESS-CHURN axis the health plane never had (debt seq1317 / seq1318).
2//
3// WHY THIS EXISTS: 2026-07-30, nx_health said {overall:OK, degraded:0, down:0} with all 14 services UP
4// while the NAS was forking 78.3 processes/sec sustained, burning 231 permil of an 8-core box in the
5// KERNEL against 98 permil in userspace (a 2.36x kernel:user inversion) and taking 91718 context
6// switches/sec. nx_resmon owns MEMORY, nx_netobs owns NETWORK -- the process-churn axis had NO organ, so
7// the largest CPU consumer on the platform was invisible to every instrument we owned and was found by
8// hand. This is that instrument.
9// LAW: liveness is not performance. A board that only asks "is it up" cannot see the machine burning.
10//
11// WHAT IT MEASURES (two /proc/stat samples bracketing a real window -- deltas, never since-boot totals,
12// because a since-boot average hides a storm that started an hour ago):
13// forks/sec -- `processes` delta. THE headline: fork+exec is the most expensive thing a kernel does.
14// ctxsw/sec -- `ctxt` delta. Rises with churn AND with thrash; corroborates, never diagnoses alone.
15// intr/sec -- `intr` delta, first field. Device pressure, reported for context (not thresholded).
16// kernel:user -- system:user+nice jiffies as permil. 1000 = parity. THE inversion detector.
17// busy/idle -- share of measured capacity, self-normalising (no HZ and no core count needed --
18// every share is a fraction of the SAME jiffy total, so the arithmetic cannot drift
19// when the box changes size. That is what makes this instrument hardware-portable).
20//
21// Policy lives in nx_procchurn_lib.nx so the gate exercises the SAME code (rule 9 + rule 15); thresholds
22// are data-driven from knowledge/status/procchurn.conf (rule 11). Read-only. No hw writes (Rule 26).
23// Exit code IS the verdict so a caller can gate on it: 0=GREEN 1=AMBER 2=RED 3=UNMEASURED.
24// license_tier: ORIGINAL expect_exit: 0
25import "nx_procchurn_lib.nx"
26
27const PC_STATBUF: i64 = 8192
28const PC_CONFBUF: i64 = 4096
29const PC_FIELD_BYTES: i64 = 256
30const PC_NCPU_FIELDS: i64 = 10
31const PC_DEF_WINDOW_MS: i64 = 3000
32const PC_MIN_WINDOW_MS: i64 = 1000
33const PC_MAX_WINDOW_MS: i64 = 10000
34const PC_US_PER_MS: i64 = 1000
35const PC_DEF_F_AMBER: i64 = 20
36const PC_DEF_F_RED: i64 = 50
37const PC_DEF_R_AMBER: i64 = 1000
38const PC_DEF_R_RED: i64 = 1500
39const PC_DEF_C_AMBER: i64 = 20000
40const PC_DEF_C_RED: i64 = 50000
41const PC_EXIT_UNMEASURED: i64 = 3
42const PC_JBUF: i64 = 512
43const PC_MODE_644: i64 = 420
44// ---- OB5 constants. PC_MS_PER_S is declared separately from PC_US_PER_MS although both are 1000:
45// ---- one constant serving two unrelated purposes can never be moved for either of them.
46const PC_SEEK_SET: i64 = 0
47const PC_SEEK_END: i64 = 2
48const PC_MS_PER_S: i64 = 1000
49// Two measurement windows of margin. The frame this run just appended is at most ONE window old, so a
50// bar of two windows can only be blown by a frame this run did NOT write -- which is precisely the
51// condition (a silently failed append) that the freshness check exists to catch.
52const PC_ACT_AGE_WINDOWS: i64 = 2
53// Journal tail read for the consumer. Sized to hold many frames, not one: a partial final write must not
54// be able to push the last COMPLETE frame out of the window, and reading the head of an append-only file
55// would sample its past rather than its present.
56const PC_ACTTAIL: i64 = 4096
57// OB4 bounds. PC_PIDCAP sits above this host measured process count (780 on 2026-09-03) with headroom,
58// and reaching it sets a cap_hit flag that turns the count into a declared FLOOR rather than a silent
59// total. PC_TALLY_MAX bounds the distinct-parent table: a storm from many parents is reported truncated
60// and SAYS so, which is the only honest failure a fixed table has.
61// PC_PATHBUF and PC_STATBUF are NOT redeclared here -- the lib and this organ already own one each.
62const PC_PIDCAP: i64 = 4096
63const PC_PIDS_BYTES: i64 = 32768
64const PC_TALLY_MAX: i64 = 64
65const PC_TALLY_BYTES: i64 = 512
66
67// APPEND one frame to the trend journal. Append-only and never truncating: history is sacred (rule 13),
68// and the burst structure this organ measures is only visible ACROSS runs. Failure to journal is NOT
69// fatal -- a read-only or full filesystem must not cost us the live measurement -- but it IS reported,
70// because a silently-missing journal would make the trend look flat instead of absent.
71func pc_jappend(path: *u8, buf: *u8, n: i64) -> i64 {
72 let fd: i64 = sys_openat_append(path, PC_MODE_644)
73 if fd < 0 { return 0 - 1 }
74 let w: i64 = sys_write(fd, buf, n)
75 sys_close(fd)
76 return w
77}
78
79// read one scalar counter line ("processes 4153147") -- returns the value or -1 FAIL-CLOSED.
80func pc_scalar(buf: *u8, n: i64, key: *u8, out: *i64) -> i64 {
81 let c: i64 = pc_line_ints(buf, n, key, out, 1)
82 if c < 1 { return 0 - 1 }
83 return out[0]
84}
85
86func main() -> i64 {
87 let cbuf: *u8 = sys_mmap(PC_CONFBUF)
88 let cn: i64 = rm_read("knowledge/status/procchurn.conf" as *u8, cbuf, PC_CONFBUF)
89 var win_ms: i64 = rm_conf(cbuf, cn, "window-ms" as *u8, PC_DEF_WINDOW_MS)
90 if win_ms < PC_MIN_WINDOW_MS { win_ms = PC_MIN_WINDOW_MS }
91 if win_ms > PC_MAX_WINDOW_MS { win_ms = PC_MAX_WINDOW_MS }
92 let f_amber: i64 = rm_conf(cbuf, cn, "forks-per-sec-amber" as *u8, PC_DEF_F_AMBER)
93 let f_red: i64 = rm_conf(cbuf, cn, "forks-per-sec-red" as *u8, PC_DEF_F_RED)
94 let r_amber: i64 = rm_conf(cbuf, cn, "sysuser-permil-amber" as *u8, PC_DEF_R_AMBER)
95 let r_red: i64 = rm_conf(cbuf, cn, "sysuser-permil-red" as *u8, PC_DEF_R_RED)
96 let c_amber: i64 = rm_conf(cbuf, cn, "ctxsw-per-sec-amber" as *u8, PC_DEF_C_AMBER)
97 let c_red: i64 = rm_conf(cbuf, cn, "ctxsw-per-sec-red" as *u8, PC_DEF_C_RED)
98
99 let b1: *u8 = sys_mmap(PC_STATBUF)
100 let b2: *u8 = sys_mmap(PC_STATBUF)
101 let c1: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
102 let c2: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
103 let sc: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
104
105 // ---- SAMPLE 1 -> window -> SAMPLE 2. The window is measured, never assumed: sys_sleep_ms can
106 // ---- oversleep on a loaded box, and using the REQUESTED window would inflate every rate.
107 let t1: i64 = sys_now_us()
108 let n1: i64 = rm_read("/proc/stat" as *u8, b1, PC_STATBUF)
109 let p1: i64 = pc_scalar(b1, n1, "processes " as *u8, sc)
110 let x1: i64 = pc_scalar(b1, n1, "ctxt " as *u8, sc)
111 let i1: i64 = pc_scalar(b1, n1, "intr " as *u8, sc)
112 let nf1: i64 = pc_line_ints(b1, n1, "cpu " as *u8, c1, PC_NCPU_FIELDS)
113 // OB4: the pid set BEFORE the window. getdents only -- no per-process reads here.
114 let prev: *i64 = sys_mmap(PC_PIDS_BYTES) as *i64
115 let pfull: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
116 let nprev: i64 = pc_pids(prev, PC_PIDCAP, pfull)
117 sys_sleep_ms(win_ms)
118 let t2: i64 = sys_now_us()
119 let n2: i64 = rm_read("/proc/stat" as *u8, b2, PC_STATBUF)
120 let p2: i64 = pc_scalar(b2, n2, "processes " as *u8, sc)
121 let x2: i64 = pc_scalar(b2, n2, "ctxt " as *u8, sc)
122 let i2: i64 = pc_scalar(b2, n2, "intr " as *u8, sc)
123 let nf2: i64 = pc_line_ints(b2, n2, "cpu " as *u8, c2, PC_NCPU_FIELDS)
124 let elapsed_ms: i64 = (t2 - t1) / PC_US_PER_MS
125 // ---- OB4: WHO IS FORKING. Attribute the processes that appeared inside this window to their
126 // ---- PARENTS, and publish it as a DECLARED LOWER BOUND beside the kernel counter.
127 // COST, BUDGETED BEFORE IT WAS WRITTEN RATHER THAN AFTER: the enumeration is getdents on /proc, a
128 // handful of syscalls regardless of process count. Only the pids that are NEW get a stat read, and
129 // at this host measured fork rate that is a few dozen per window -- roughly 50 syscalls per beat, not
130 // the ~2300 a full per-process walk would cost. That arithmetic is why this runs on every beat
131 // instead of hiding behind a severity check: an instrument must not become the load it measures, and
132 // the way to honour that is to size the work before writing it, not to add a switch afterwards.
133 let cur: *i64 = sys_mmap(PC_PIDS_BYTES) as *i64
134 let cfull: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
135 let ncur: i64 = pc_pids(cur, PC_PIDCAP, cfull)
136 let tpar: *i64 = sys_mmap(PC_TALLY_BYTES) as *i64
137 let tcnt: *i64 = sys_mmap(PC_TALLY_BYTES) as *i64
138 let sb4: *u8 = sys_mmap(PC_STATBUF)
139 let pb4: *u8 = sys_mmap(PC_PATHBUF)
140 let so4: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
141 let ao4: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
142 var attr_ok: i64 = 0
143 if nprev >= 0 { if ncur >= 0 { attr_ok = 1 } }
144 if attr_ok == 1 { pc_fork_by_parent(prev, nprev, cur, ncur, tpar, tcnt, PC_TALLY_MAX, sb4, pb4, so4, ao4) }
145
146 var forks_ps: i64 = 0 - 1
147 if p1 >= 0 { if p2 >= 0 { forks_ps = pc_rate(p2 - p1, elapsed_ms) } }
148 var ctxsw_ps: i64 = 0 - 1
149 if x1 >= 0 { if x2 >= 0 { ctxsw_ps = pc_rate(x2 - x1, elapsed_ms) } }
150 var intr_ps: i64 = 0 - 1
151 if i1 >= 0 { if i2 >= 0 { intr_ps = pc_rate(i2 - i1, elapsed_ms) } }
152
153 var cpu_ok: i64 = 0
154 if nf1 >= PC_NCPU_FIELDS { if nf2 >= PC_NCPU_FIELDS { cpu_ok = 1 } }
155 var usr_d: i64 = 0
156 var sys_d: i64 = 0
157 var irq_d: i64 = 0
158 var idle_d: i64 = 0
159 var iow_d: i64 = 0
160 var tot_d: i64 = 0
161 if cpu_ok == 1 {
162 var k: i64 = 0
163 while k < PC_NCPU_FIELDS { tot_d = tot_d + (c2[k] - c1[k]); k = k + 1 }
164 usr_d = (c2[0] - c1[0]) + (c2[1] - c1[1])
165 sys_d = c2[2] - c1[2]
166 idle_d = c2[3] - c1[3]
167 iow_d = c2[4] - c1[4]
168 irq_d = (c2[5] - c1[5]) + (c2[6] - c1[6])
169 }
170 var ratio_pm: i64 = 0 - 1
171 if cpu_ok == 1 { ratio_pm = pc_ratio_permil(sys_d, usr_d) }
172 var usr_pm: i64 = 0 - 1
173 var sys_pm: i64 = 0 - 1
174 var irq_pm: i64 = 0 - 1
175 var busy_pm: i64 = 0 - 1
176 if tot_d > 0 {
177 usr_pm = (usr_d * 1000) / tot_d
178 sys_pm = (sys_d * 1000) / tot_d
179 irq_pm = (irq_d * 1000) / tot_d
180 busy_pm = ((tot_d - idle_d - iow_d) * 1000) / tot_d
181 }
182
183 // FAIL-CLOSED: a negative (unmeasurable) axis compares BELOW every threshold and would otherwise be
184 // published as a confident GREEN. That is the partial-as-complete defect the operator banned, so an
185 // incomplete sample gets its OWN exit code and prints no verdict at all.
186 var measured: i64 = 1
187 if forks_ps < 0 { measured = 0 }
188 if ctxsw_ps < 0 { measured = 0 }
189 if ratio_pm < 0 { measured = 0 }
190 if cpu_ok == 0 { measured = 0 }
191 if tot_d <= 0 { measured = 0 }
192
193 rm_puts("=== nx_procchurn -- the process-churn axis nx_health lacks (seq1317) ===\n" as *u8)
194 rm_puts("window_ms=" as *u8); rm_num(elapsed_ms)
195 rm_puts(" (requested " as *u8); rm_num(win_ms); rm_puts(", MEASURED not assumed)\n" as *u8)
196 if measured == 0 {
197 rm_puts("verdict=UNMEASURED sev=" as *u8); rm_num(PC_EXIT_UNMEASURED)
198 rm_puts(" why=a /proc/stat counter was absent or went backwards (wrap/reboot); NO verdict is" as *u8)
199 rm_puts(" published off a partial sample\n" as *u8)
200 return PC_EXIT_UNMEASURED
201 }
202 rm_puts("forks_per_sec=" as *u8); rm_num(forks_ps)
203 rm_puts(" (amber>=" as *u8); rm_num(f_amber); rm_puts(" red>=" as *u8); rm_num(f_red); rm_puts(")\n" as *u8)
204 rm_puts("ctxsw_per_sec=" as *u8); rm_num(ctxsw_ps)
205 rm_puts(" (amber>=" as *u8); rm_num(c_amber); rm_puts(" red>=" as *u8); rm_num(c_red); rm_puts(")\n" as *u8)
206 rm_puts("intr_per_sec=" as *u8); rm_num(intr_ps); rm_puts(" (context only, not thresholded)\n" as *u8)
207 rm_puts("kernel_user_permil=" as *u8); rm_num(ratio_pm)
208 rm_puts(" (1000=parity; amber>=" as *u8); rm_num(r_amber); rm_puts(" red>=" as *u8); rm_num(r_red); rm_puts(")\n" as *u8)
209 rm_puts("cpu_share_permil: user=" as *u8); rm_num(usr_pm)
210 rm_puts(" system=" as *u8); rm_num(sys_pm)
211 rm_puts(" irq_softirq=" as *u8); rm_num(irq_pm)
212 rm_puts(" busy=" as *u8); rm_num(busy_pm); rm_puts("\n" as *u8)
213 rm_puts("jiffies_delta: user+nice=" as *u8); rm_num(usr_d)
214 rm_puts(" system=" as *u8); rm_num(sys_d)
215 rm_puts(" total=" as *u8); rm_num(tot_d); rm_puts("\n" as *u8)
216
217 // QUEUE COMPOSITION (seq1555) -- the number that separates an I/O-blocked queue from a compute one,
218 // and it is FREE: /proc/stat already carries procs_running and procs_blocked, so this costs ZERO extra
219 // syscalls and needs NO admission gate (unlike a /proc walk, which is refused exactly when a deep
220 // queue makes it most interesting -- the priority-lane trap).
221 // WHY IT MATTERS: loadavg counts TASK_UNINTERRUPTIBLE as well as runnable, so a load of 14.94 against
222 // a 50.7pct-idle CPU says the QUEUE is deep but never says WHAT it waits on. iowait cannot answer
223 // either -- Linux only accrues it while a CPU is otherwise IDLE, so a D-state task on a busy core is
224 // invisible to it. procs_blocked counts those tasks DIRECTLY.
225 // READ IT AS: blocked >> running => the queue is I/O/lock-bound, and adding CPU would change nothing.
226 var blocked: i64 = 0 - 1
227 var running: i64 = 0 - 1
228 if pc_line_ints(b2, n2, "procs_blocked " as *u8, sc, 1) > 0 { blocked = sc[0] }
229 if pc_line_ints(b2, n2, "procs_running " as *u8, sc, 1) > 0 { running = sc[0] }
230 rm_puts("queue: procs_running=" as *u8); rm_num(running)
231 rm_puts(" procs_blocked=" as *u8); rm_num(blocked)
232 rm_puts(" (blocked >> running = I/O or lock bound, NOT compute; more CPU would not help)\n" as *u8)
233
234 // OB4 REPORT. The coverage line is the whole point: forks_attributed is a census of SURVIVORS and
235 // kernel_forks is the true count, so the ratio says how much of the churn this attribution can even
236 // see. A low ratio is not an error -- it is the short-lived majority, and it is the reason a pid diff
237 // must never be published as a fork rate. When /proc could not be enumerated the axis says so rather
238 // than printing a zero, because zero attributed and zero looked at are opposite findings.
239 if attr_ok == 0 { rm_puts("forkattrib=UNOBSERVABLE (/proc could not be enumerated; NOT the same as nothing forked)\n" as *u8) }
240 if attr_ok == 1 {
241 let kforks: i64 = p2 - p1
242 rm_puts("forkattrib=LOWER-BOUND new_survivors=" as *u8); rm_num(ao4[0])
243 rm_puts(" attributed=" as *u8); rm_num(ao4[1])
244 rm_puts(" distinct_parents=" as *u8); rm_num(ao4[2])
245 rm_puts(" vanished_before_read=" as *u8); rm_num(ao4[3])
246 rm_puts(" kernel_forks=" as *u8); rm_num(kforks)
247 rm_puts(" coverage_permil=" as *u8); rm_num(pc_attrib_permil(ao4[1], kforks))
248 rm_puts(" pids_seen=" as *u8); rm_num(ncur)
249 rm_puts(" cap_hit=" as *u8); rm_num(cfull[0])
250 rm_puts(" (a SURVIVOR census: the short-lived majority is invisible to it BY CONSTRUCTION, so coverage_permil is how much of the kernel fork count this attribution could account for -- never read the attributed number as a fork rate)\n" as *u8)
251 var tp4: i64 = 0
252 while tp4 < ao4[2] {
253 rm_puts(" parent ppid=" as *u8); rm_num(tpar[tp4])
254 rm_puts(" spawned=" as *u8); rm_num(tcnt[tp4])
255 rm_puts("\n" as *u8)
256 tp4 = tp4 + 1
257 }
258 }
259 let sev: i64 = pc_verdict(forks_ps, ratio_pm, ctxsw_ps, f_amber, f_red, r_amber, r_red, c_amber, c_red)
260
261 // ---- TREND: one append-only frame per run. A single verdict is a snapshot; the storm is bursty,
262 // ---- so only the journal can show whether a fix actually landed.
263 let jb: *u8 = sys_mmap(PC_JBUF)
264 let jn: i64 = pc_frame(jb, sys_now_realtime_sec(), elapsed_ms, forks_ps, ctxsw_ps, intr_ps,
265 ratio_pm, usr_pm, sys_pm, busy_pm, sev, running, blocked)
266 let jw: i64 = pc_jappend("knowledge/status/procchurn.jrnl" as *u8, jb, jn)
267 rm_puts("journal=knowledge/status/procchurn.jrnl " as *u8)
268 if jw < 0 { rm_puts("APPEND-FAILED (measurement still valid; the TREND is what was lost)\n" as *u8) }
269 if jw >= 0 { rm_puts("frame_bytes=" as *u8); rm_num(jw); rm_puts("\n" as *u8) }
270
271 // ---- OB5: CONSUME THE SEVERITY WE JUST WROTE -- the actuator half of a loop that had only a sensor.
272 // It reads the frame back OFF DISK rather than reusing `sev` above, ON PURPOSE: reading the live
273 // variable would prove the arithmetic and prove nothing about the WIRE, and the wire is exactly where
274 // a producer and a consumer that are each correct in isolation still disagree. If the append silently
275 // failed, this reads the PREVIOUS frame, its age blows the bar, and the action is UNOBSERVABLE --
276 // which is the honest answer, and is one a read of the in-memory value could never have produced.
277 let ab: *u8 = sys_mmap(PC_ACTTAIL)
278 var an: i64 = 0
279 let afd: i64 = sys_openat_rd("knowledge/status/procchurn.jrnl" as *u8)
280 if afd >= 0 {
281 let asz: i64 = sys_lseek(afd, 0, PC_SEEK_END)
282 var aoff: i64 = 0
283 if asz > PC_ACTTAIL { aoff = asz - PC_ACTTAIL }
284 sys_lseek(afd, aoff, PC_SEEK_SET)
285 an = sys_read(afd, ab, PC_ACTTAIL)
286 sys_close(afd)
287 }
288 if an < 0 { an = 0 }
289 let ao: *i64 = sys_mmap(PC_FIELD_BYTES) as *i64
290 let ard: i64 = pc_sev_last(ab, an, ao)
291 // THE STALENESS BAR IS DERIVED, NOT PICKED. The frame this run just appended is at most one
292 // measurement window old, so two windows leaves a full window of margin -- the same shape as a drain
293 // deadline that follows its socket timeout rather than inventing a number. A conf row OVERRIDES the
294 // derivation, and the provenance is PRINTED so the next reader never has to guess which one won.
295 var act_max_age: i64 = (win_ms / PC_MS_PER_S) * PC_ACT_AGE_WINDOWS
296 var act_prov: *u8 = "DERIVED-from-window" as *u8
297 let act_conf: i64 = rm_conf(cbuf, cn, "act-max-age-s" as *u8, 0)
298 if act_conf > 0 { act_max_age = act_conf; act_prov = "CONF" as *u8 }
299 var disk_sev: i64 = 0 - 1
300 var disk_age: i64 = 0 - 1
301 if ard == 1 { disk_sev = ao[0]; disk_age = sys_now_realtime_sec() - ao[1] }
302 let act: i64 = pc_sev_consumer(disk_sev, disk_age, act_max_age)
303 rm_puts("act=" as *u8); rm_puts(pc_act_name(act))
304 rm_puts(" read=" as *u8)
305 if ard == 1 { rm_puts("FRAME" as *u8) }
306 if ard == 0 { rm_puts("NO-FRAME" as *u8) }
307 if ard < 0 { rm_puts("UNPARSEABLE" as *u8) }
308 rm_puts(" disk_sev=" as *u8); rm_num(disk_sev)
309 rm_puts(" age_s=" as *u8); rm_num(disk_age)
310 rm_puts(" max_age_s=" as *u8); rm_num(act_max_age)
311 rm_puts(" bar=" as *u8); rm_puts(act_prov)
312 rm_puts("\n" as *u8)
313 // TRUNCATE-write, never append. A pre-deploy check that greps an append-only journal is vacuously
314 // green FOREVER, because a marker that ever appeared still matches; that is measured estate law, not
315 // a preference. A rewritten file carrying one canonical line is the only shape a consumer can read.
316 let sb: *u8 = sys_mmap(PC_JBUF)
317 var so: i64 = 0
318 so = pc_cat(sb, so, "PROCCHURN-ACT act=" as *u8)
319 so = pc_cat(sb, so, pc_act_name(act))
320 so = pc_cat(sb, so, " disk_sev=" as *u8)
321 so = pc_catn(sb, so, disk_sev)
322 so = pc_cat(sb, so, " age_s=" as *u8)
323 so = pc_catn(sb, so, disk_age)
324 so = pc_cat(sb, so, " max_age_s=" as *u8)
325 so = pc_catn(sb, so, act_max_age)
326 so = pc_cat(sb, so, " ts=" as *u8)
327 so = pc_catn(sb, so, sys_now_realtime_sec())
328 so = pc_cat(sb, so, "\n" as *u8)
329 let sfd: i64 = sys_openat_wr("knowledge/status/procchurn.act" as *u8, PC_MODE_644)
330 if sfd >= 0 { sys_write(sfd, sb, so); sys_close(sfd) }
331 if sfd < 0 { rm_puts("act_status=WRITE-FAILED (the action stands; only its durable receipt was lost)\n" as *u8) }
332
333 rm_puts("verdict=" as *u8)
334 if sev == 0 { rm_puts("GREEN" as *u8) }
335 if sev == 1 { rm_puts("AMBER" as *u8) }
336 if sev == 2 { rm_puts("RED" as *u8) }
337 rm_puts(" sev=" as *u8); rm_num(sev); rm_puts("\n" as *u8)
338 // A RED/AMBER churn axis names no producer -- point the operator at the tool that does, so the
339 // on-demand diagnosis path is self-documenting (nx_ctxtop classifies SYSTEM=DSM / ESTATE=nx_* /
340 // KERNEL, 2026-08-12). Only on AMBER/RED: a GREEN box needs no producer hunt.
341 if sev >= 1 { rm_puts("next=run nx_ctxtop -- it names WHO produces the churn and classifies it (SYSTEM=DSM / ESTATE=nx_* / KERNEL); this axis is not GREEN but does not name the producer\n" as *u8) }
342 return sev
343}