nx_jobclaim_reap.nx source
↩ module page · 185 lines · 8419 B
1// nx_jobclaim_reap.nx -- THE SANCTIONED WAY TO RETIRE A DEAD _jobs CLAIM.
2//
3// WHY THIS EXISTS (debt 1787259805, measured 2026-08-20). nx_joblost_gate correctly reports LOST=4
4// against a ratchet floor of 1 and is therefore RED. The four claims are real: ids 1786132743,
5// 1786239344, 1786732031 and 1786927039, aged 3.8 to 13.0 days, reservations that were made and
6// never came back. THE GATE IS CORRECT AND THE FLOOR MUST NOT BE RAISED -- loosening a ratchet to
7// flatter a number is editing the detector instead of fixing what it found.
8// The blocker was that the estate had NO VERB for removing a dead claim: nx_retire_path refuses
9// `_jobs/x` as too shallow, and nx_capsearch over 1,187 tools finds no incumbent (nx_orphan_reap
10// reaps DAEMONS by cmdline, nx_tmpstore_reap reaps seg-store FAMILIES, nx_claims reap works the
11// session-lease JOURNAL, nx_toolreg_reap works allowlist ROWS). Four things were blocked by one
12// missing verb: the gate cannot be bite-proven while its baseline is RED, so nx_gate_bite reports
13// UNCONTROLLED, so nx_gate_roster_run correctly refuses to admit it to the 630-row roster.
14//
15// THIS ORGAN IS THE ACTUATOR, NOT THE CENSUS. It takes ids and acts on exactly those.
16// The WORKLIST comes from nx_joblost_gate, which already scans the plane and prints
17// `LOST job_<id> age=<n>s`. Building a second directory scanner here would be a duplicate ruler,
18// and a sweep verb would make every reap an unnamed bulk act instead of an auditable one.
19//
20// IT DOES NOT DELETE. It APPENDS a tombstone (rule 13): the claim keeps its original marker and
21// gains `state=REAPED ts=<now> age=<n>s by=nx_jobclaim_reap`. The reader that must understand that
22// tombstone -- nx_joblost_gate -- imports the SAME classifier from nx_jobclaim_lib, because a
23// reaper shipped alone is a no-op that looks like a fix.
24//
25// EVERY REFUSAL NAMES ITS RULE. ABSENT, DONE, HAS-OUT, TOO-YOUNG and UNPARSED are five different
26// answers with five different remedies; a single "refused" would send the reader to re-derive by
27// hand the examination this organ just performed.
28//
29// license_tier: ORIGINAL expect_exit: 0
30import "nx_syscalls.nx"
31import "nx_jobclaim_lib.nx"
32
33const JRC_EXIT_OK: i64 = 0
34const JRC_EXIT_REFUSED: i64 = 1
35const JRC_EXIT_USAGE: i64 = 2
36const JRC_EXIT_BLOCKED: i64 = 3
37const JRC_ARGV_VERB: i64 = 1
38const JRC_ARGV_FIRST: i64 = 2
39const JRC_MIN_ARGC: i64 = 3
40
41func jrc_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
42
43func jrc_n(v: i64) -> i64 {
44 let b: *u8 = sys_mmap(JR_TOMB_MAX)
45 let p: i64 = jr_catnum(b, 0, v)
46 sys_write(1, b, p)
47 return 0
48}
49
50func jrc_usage() -> i64 {
51 jrc_w("usage: nx_jobclaim_reap check <job-id> [<job-id>...] [maxage=<sec>] [dir=<path>]\n" as *u8)
52 jrc_w(" nx_jobclaim_reap reap <job-id> [<job-id>...] [maxage=<sec>] [dir=<path>]\n" as *u8)
53 jrc_w(" check DECIDES and writes nothing; reap APPENDS a tombstone to every eligible claim.\n" as *u8)
54 jrc_w(" The WORKLIST is nx_joblost_gate's LOST rows -- this organ actuates them, it is not a census.\n" as *u8)
55 jrc_w(" dir defaults to _jobs and is refused outside _jobs and /tmp/ (blast radius).\n" as *u8)
56 jrc_w(" exit 0 every named id reaped or already reaped | 1 at least one refusal | 2 usage | 3 dir refused\n" as *u8)
57 return 0
58}
59
60// "<key>=<value>" -> pointer to <value>, or 0 when this argument is not that key.
61func jrc_opt(arg: *u8, key: *u8) -> *u8 {
62 let k: i64 = jr_slen(key)
63 let n: i64 = jr_slen(arg)
64 if n <= k { return 0 as *u8 }
65 var i: i64 = 0
66 while i < k { if arg[i] != key[i] { return 0 as *u8 } i = i + 1 }
67 return ((arg as i64) + k) as *u8
68}
69
70func main(argc: i64, argv: *i64) -> i64 {
71 if argc < JRC_MIN_ARGC { jrc_usage(); sys_exit(JRC_EXIT_USAGE) }
72
73 let verb: *u8 = argv[JRC_ARGV_VERB] as *u8
74 var dry: i64 = 0 - 1
75 if jr_streq(verb, "check" as *u8) == 1 { dry = 1 }
76 if jr_streq(verb, "reap" as *u8) == 1 { dry = 0 }
77 if dry < 0 {
78 jrc_w("REFUSED-BAD-VERB: \"" as *u8); jrc_w(verb)
79 jrc_w("\" -- the verbs are check and reap\n" as *u8)
80 jrc_usage()
81 sys_exit(JRC_EXIT_USAGE)
82 }
83
84 // ---- options first, so the threshold and the directory are settled before any id is examined.
85 let dflt: *i64 = sys_mmap(JR_SCRATCH) as *i64
86 let sc: *i64 = sys_mmap(JR_SCRATCH) as *i64
87 var maxage: i64 = jr_maxage(dflt, sc)
88 var maxage_src: *u8 = "conf" as *u8
89 if dflt[0] == 1 { maxage_src = "code-default (conf absent or unparseable, stated not hidden)" as *u8 }
90 var dir: *u8 = JR_JOBS
91
92 var i: i64 = JRC_ARGV_FIRST
93 while i < argc {
94 let a: *u8 = argv[i] as *u8
95 let mv: *u8 = jrc_opt(a, "maxage=" as *u8)
96 if (mv as i64) != 0 {
97 let v: i64 = jr_atoi_strict(mv)
98 if v < 0 {
99 jrc_w("REFUSED-BAD-MAXAGE: \"" as *u8); jrc_w(mv)
100 jrc_w("\" is not a positive integer -- a zero or negative threshold would reap everything\n" as *u8)
101 sys_exit(JRC_EXIT_USAGE)
102 }
103 maxage = v
104 maxage_src = "argv (highest priority in the configuration hierarchy)" as *u8
105 }
106 let dv: *u8 = jrc_opt(a, "dir=" as *u8)
107 if (dv as i64) != 0 { dir = dv }
108 i = i + 1
109 }
110
111 if jr_dir_allowed(dir) == 0 {
112 jrc_w("REFUSED-DIR: \"" as *u8); jrc_w(dir)
113 jrc_w("\" -- this organ writes only inside the _jobs plane or under /tmp/ , and never through \"..\".\n" as *u8)
114 jrc_w(" A reaper that will append anywhere is a different and much larger tool than this one.\n" as *u8)
115 sys_exit(JRC_EXIT_BLOCKED)
116 }
117
118 let now: i64 = sys_now_realtime_sec()
119 jrc_w("=== NX-JOBCLAIM-REAP verb=" as *u8); jrc_w(verb)
120 jrc_w(" dir=" as *u8); jrc_w(dir)
121 jrc_w(" now=" as *u8); jrc_n(now)
122 jrc_w(" maxage=" as *u8); jrc_n(maxage)
123 jrc_w("s from " as *u8); jrc_w(maxage_src)
124 jrc_w(" ===\n" as *u8)
125 if dry == 1 { jrc_w(" DRY RUN -- deciding only, nothing is written.\n" as *u8) }
126
127 let info: *i64 = sys_mmap(JR_I_SLOTS * JR_I64_BYTES) as *i64
128 let pbuf: *u8 = sys_mmap(JR_PATH)
129 let line: *u8 = sys_mmap(JR_LINE)
130
131 var examined: i64 = 0
132 var reaped: i64 = 0
133 var already: i64 = 0
134 var refused: i64 = 0
135
136 i = JRC_ARGV_FIRST
137 while i < argc {
138 let a: *u8 = argv[i] as *u8
139 var is_opt: i64 = 0
140 if (jrc_opt(a, "maxage=" as *u8) as i64) != 0 { is_opt = 1 }
141 if (jrc_opt(a, "dir=" as *u8) as i64) != 0 { is_opt = 1 }
142 if is_opt == 0 {
143 examined = examined + 1
144 let id: i64 = jr_atoi_strict(a)
145 var d: i64 = JR_D_BADID
146 if id > 0 { d = jr_reap_one(dir, id, now, maxage, dry, info, pbuf, JR_PATH, sc, line, JR_LINE) }
147
148 jrc_w(" " as *u8); jrc_w(jr_decision_name(d))
149 jrc_w(" job_" as *u8); jrc_w(a)
150 if id > 0 {
151 jrc_w(" state=" as *u8); jrc_w(jr_state_name(info[JR_I_STATE]))
152 jrc_w(" bytes=" as *u8); jrc_n(info[JR_I_BYTES])
153 jrc_w(" age=" as *u8); jrc_n(info[JR_I_AGE])
154 jrc_w("s has_out=" as *u8); jrc_n(info[JR_I_HASOUT])
155 }
156 jrc_w("\n " as *u8); jrc_w(jr_decision_why(d)); jrc_w("\n" as *u8)
157
158 if d == JR_D_REAP { reaped = reaped + 1 }
159 if d == JR_D_ALREADY { already = already + 1 }
160 if d > JR_D_ALREADY { refused = refused + 1 }
161 }
162 i = i + 1
163 }
164
165 if examined == 0 {
166 jrc_w("REFUSED-NO-IDS: every argument was an option -- name at least one job id\n" as *u8)
167 jrc_usage()
168 sys_exit(JRC_EXIT_USAGE)
169 }
170
171 // A PARTITION IS A CLAIM: CHECK THE PARTS SUM, and print the sum so a reader can check it too.
172 let sum: i64 = reaped + already + refused
173 jrc_w(" examined=" as *u8); jrc_n(examined)
174 if dry == 1 { jrc_w(" would_reap=" as *u8) } else { jrc_w(" reaped=" as *u8) }
175 jrc_n(reaped)
176 jrc_w(" already=" as *u8); jrc_n(already)
177 jrc_w(" refused=" as *u8); jrc_n(refused)
178 jrc_w(" sum=" as *u8); jrc_n(sum)
179 if sum == examined { jrc_w(" partition=RECONCILES\n" as *u8) } else { jrc_w(" partition=LEAKS\n" as *u8) }
180 if sum != examined { sys_exit(JRC_EXIT_BLOCKED) }
181
182 if refused > 0 { sys_exit(JRC_EXIT_REFUSED) }
183 sys_exit(JRC_EXIT_OK)
184 return 0
185}