code wiki / _hdl_build / nx_gate_loop.nx
nx_gate_loop.nx source
↩ module page · 278 lines · 10050 B
1// nx_gate_loop.nx -- TEAM-OWNED loop daemon that runs nx_gate_sentinel on a
2// cadence (GAME arc step G5: "wire the sentinel into the loop cadence so
3// grades refresh with no human/LLM trigger"). Closes the loop-monitor
4// lesson from [[project-self-running-loop-governed-ingest-2026-06-05]]: a
5// loop must have a MONITORED OUTPUT CHANNEL -- every beat appends one line
6// to /tmp/nishi_game_gate_loop.log, and the sentinel itself escalates
7// regressions to the PM review log, so a DRY or STALLED loop is visible in
8// the logs, not silent.
9//
10// Usage (from nxc2 repo root):
11// nx_gate_loop.elf # production: 30-min beat, forever, daemonized
12// nx_gate_loop.elf <beat_ms> # custom beat, forever, daemonized
13// nx_gate_loop.elf <beat_ms> <n_beats> # TEST MODE: n beats, FOREGROUND (no daemonize)
14// Daemonized = fork + setsid; the launcher returns immediately and the loop
15// survives the launching terminal (pattern from the sites-daemon arc).
16//
17// license_tier: ORIGINAL
18import "nx_syscalls.nx"
19
20const GL_DEFAULT_BEAT_MS: i64 = 1800000 // 30 min
21const GL_LOG: i64 = 0
22
23func gl_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i }
24func gl_cat_n(dst: *u8, off: i64, v: i64) -> i64 {
25 var o: i64 = off
26 var m: i64 = v
27 if m < 0 { dst[o] = 45; o = o + 1; m = 0 - m }
28 let t: *u8 = sys_mmap(28)
29 var k: i64 = 0
30 if m == 0 { t[0] = 48; k = 1 }
31 while m > 0 { t[k] = 48 + (m % 10); m = m / 10; k = k + 1 }
32 var i: i64 = 0
33 while i < k { dst[o+i] = t[k-1-i]; i = i + 1 }
34 return o + k
35}
36
37// parse a decimal arg; -1 if absent/garbage
38func gl_atoi(s: *u8) -> i64 {
39 if s == (0 as *u8) { return 0 - 1 }
40 var v: i64 = 0
41 var any: i64 = 0
42 var i: i64 = 0
43 while s[i] != (0 as u8) {
44 let c: i64 = s[i] as i64
45 if c < 48 { return 0 - 1 }
46 if c > 57 { return 0 - 1 }
47 v = v * 10 + (c - 48)
48 any = 1
49 i = i + 1
50 }
51 if any == 0 { return 0 - 1 }
52 return v
53}
54
55// ---- singleton guard -------------------------------------------------
56// Two loop instances would run two gates CONCURRENTLY, and the gate's
57// /tmp/<module>.gg.s artifacts are shared paths -> corrupted builds ->
58// spurious REGRESSION beats (observed live on first deploy 2026-06-09:
59// the build-runner's argless run daemonized an instance, then test mode
60// raced it; one beat reported a phantom regression). Pidfile + /proc
61// aliveness check; stale pidfile (dead pid) is reclaimed.
62
63// getpid WITHOUT the syscall table: __syscall(172 rv64-getpid) returned
64// -ENOSYS live 2026-06-09 -- the rv64->x86 translation table doesn't cover
65// 172, so x86 received 172=iopl. /proc/self/stat's first field is the pid
66// and depends on no table. (Translation-table gap flagged in the PM log.)
67func gl_getpid() -> i64 {
68 let len_p: *i64 = sys_mmap(8) as *i64
69 let buf: *u8 = sys_read_file("/proc/self/stat" as *u8, len_p)
70 if (buf as i64) == 0 { return 0 - 1 }
71 let n: i64 = len_p[0]
72 var v: i64 = 0
73 var any: i64 = 0
74 var i: i64 = 0
75 while i < n {
76 let c: i64 = buf[i] as i64
77 if c >= 48 {
78 if c <= 57 { v = v * 10 + (c - 48); any = 1; i = i + 1; continue }
79 }
80 if any == 1 { i = n }
81 if any == 0 { i = i + 1 }
82 }
83 if any == 0 { return 0 - 1 }
84 return v
85}
86
87// is pid alive? openat /proc/<pid>/comm succeeds only for live processes.
88func gl_pid_alive(pid: i64) -> i64 {
89 if pid <= 0 { return 0 }
90 let p: *u8 = sys_mmap(64)
91 var o: i64 = 0
92 o = gl_cat(p, o, "/proc/" as *u8)
93 o = gl_cat_n(p, o, pid)
94 o = gl_cat(p, o, "/comm" as *u8)
95 p[o] = 0 as u8
96 let fd: i64 = sys_openat_rd(p)
97 if fd < 0 { return 0 }
98 sys_close(fd)
99 return 1
100}
101
102func gl_read_pidfile(path: *u8) -> i64 {
103 let len_p: *i64 = sys_mmap(8) as *i64
104 let buf: *u8 = sys_read_file(path, len_p)
105 if (buf as i64) == 0 { return 0 - 1 }
106 let n: i64 = len_p[0]
107 var v: i64 = 0
108 var any: i64 = 0
109 var i: i64 = 0
110 while i < n {
111 let c: i64 = buf[i] as i64
112 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1 } }
113 i = i + 1
114 }
115 if any == 0 { return 0 - 1 }
116 return v
117}
118
119// returns 1 if we acquired the lock (pidfile written with our pid), 0 if
120// another live instance holds it.
121func gl_acquire_lock() -> i64 {
122 let path: *u8 = "/tmp/nishi_game_gate_loop.pid" as *u8
123 let other: i64 = gl_read_pidfile(path)
124 if other > 0 {
125 if gl_pid_alive(other) == 1 { return 0 }
126 }
127 let fd: i64 = sys_openat_wr(path, 0x1a4)
128 if fd < 0 { return 0 }
129 let buf: *u8 = sys_mmap(32)
130 var o: i64 = gl_cat_n(buf, 0, gl_getpid())
131 buf[o] = 0x0A as u8
132 sys_write(fd, buf, o + 1)
133 sys_close(fd)
134 return 1
135}
136
137// run the sentinel once; returns its exit, or 128+sig
138func gl_run_sentinel() -> i64 {
139 let path: *u8 = "/tmp/nx_gate_sentinel.sov.elf" as *u8
140 let pid: i64 = sys_fork()
141 if pid == 0 {
142 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
143 if devnull >= 0 { sys_dup3(devnull, 1, 0) }
144 let argv: *i64 = sys_mmap(32) as *i64
145 argv[0] = path as i64; argv[1] = 0
146 let envp: *i64 = sys_mmap(16) as *i64
147 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0
148 sys_execve(path, argv, envp); sys_exit(127)
149 }
150 let st: *i64 = sys_mmap(16) as *i64
151 sys_wait4(pid, st, 0)
152 let sig: i64 = st[0] & 0x7f
153 if sig != 0 { return 128 + sig }
154 return (st[0] >> 8) & 0xff
155}
156
157func gl_beat_line(beat: i64, rc: i64) -> i64 {
158 let fd: i64 = sys_openat_append("/tmp/nishi_game_gate_loop.log" as *u8, 0x1a4)
159 if fd < 0 { return 0 - 1 }
160 let line: *u8 = sys_mmap(256)
161 var o: i64 = 0
162 o = gl_cat(line, o, "GATELOOP ts=" as *u8)
163 o = gl_cat_n(line, o, sys_now_realtime_sec())
164 o = gl_cat(line, o, " beat=" as *u8)
165 o = gl_cat_n(line, o, beat)
166 o = gl_cat(line, o, " sentinel-exit=" as *u8)
167 o = gl_cat_n(line, o, rc)
168 o = gl_cat(line, o, " (0=steady-or-better 1=REGRESSION 2=gate-crashed)" as *u8)
169 o = gl_cat(line, o, "\n" as *u8)
170 sys_write(fd, line, o)
171 sys_close(fd)
172 return 0
173}
174
175// run the user-journey gate (live-site visitor mimicry) once; best-effort --
176// a missing binary must not break the loop. Its JOURNEY lines land in
177// /tmp/nishi_user_journey.log.
178func gl_run_journey() -> i64 {
179 let path: *u8 = "/tmp/nx_user_journey.sov.elf" as *u8
180 let chk: i64 = sys_openat_rd(path)
181 if chk < 0 { return 0 - 1 }
182 sys_close(chk)
183 let pid: i64 = sys_fork()
184 if pid == 0 {
185 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
186 if devnull >= 0 { sys_dup3(devnull, 1, 0) }
187 let argv: *i64 = sys_mmap(32) as *i64
188 argv[0] = path as i64; argv[1] = 0
189 let envp: *i64 = sys_mmap(16) as *i64
190 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0
191 sys_execve(path, argv, envp); sys_exit(127)
192 }
193 let st: *i64 = sys_mmap(16) as *i64
194 sys_wait4(pid, st, 0)
195 let sig: i64 = st[0] & 0x7f
196 if sig != 0 { return 128 + sig }
197 return (st[0] >> 8) & 0xff
198}
199
200func gl_loop(beat_ms: i64, n_beats: i64) -> i64 {
201 var beat: i64 = 1
202 var go: i64 = 1
203 while go == 1 {
204 let rc: i64 = gl_run_sentinel()
205 gl_beat_line(beat, rc)
206 // live-site visitor journey each beat (polite: ~7 requests / 30 min)
207 let jrc: i64 = gl_run_journey()
208 let jfd: i64 = sys_openat_append("/tmp/nishi_game_gate_loop.log" as *u8, 0x1a4)
209 if jfd >= 0 {
210 let jl: *u8 = sys_mmap(128)
211 var jo: i64 = 0
212 jo = gl_cat(jl, jo, "GATELOOP journey-exit=" as *u8)
213 jo = gl_cat_n(jl, jo, jrc)
214 jo = gl_cat(jl, jo, "\n" as *u8)
215 sys_write(jfd, jl, jo)
216 sys_close(jfd)
217 }
218 if n_beats > 0 { if beat >= n_beats { go = 0 } }
219 if go == 1 {
220 sys_sleep_ms(beat_ms)
221 beat = beat + 1
222 }
223 }
224 return 0
225}
226
227func main(argc: i64, argv: *i64) -> i64 {
228 var beat_ms: i64 = GL_DEFAULT_BEAT_MS
229 var n_beats: i64 = 0
230 if argc >= 2 { let b: i64 = gl_atoi(argv[1] as *u8); if b > 0 { beat_ms = b } }
231 if argc >= 3 { let n: i64 = gl_atoi(argv[2] as *u8); if n > 0 { n_beats = n } }
232
233 // TEST MODE (bounded beats): run in the foreground so the harness can wait.
234 // Still takes the singleton lock -- a test run racing the production
235 // daemon corrupts shared gate artifacts just the same.
236 if n_beats > 0 {
237 if gl_acquire_lock() == 0 {
238 sys_write(1, "GATELOOP another instance is live (pidfile); refusing\n" as *u8, 54)
239 return 3
240 }
241 gl_loop(beat_ms, n_beats)
242 return 0
243 }
244
245 // PRODUCTION: daemonize (fork; parent exits; child setsid + detach stdio)
246 // -- the sites-daemon pattern: the launcher returns immediately and the
247 // loop survives the launching terminal.
248 if gl_read_pidfile("/tmp/nishi_game_gate_loop.pid" as *u8) > 0 {
249 if gl_pid_alive(gl_read_pidfile("/tmp/nishi_game_gate_loop.pid" as *u8)) == 1 {
250 sys_write(1, "GATELOOP another instance is live (pidfile); refusing\n" as *u8, 54)
251 return 3
252 }
253 }
254 let pid: i64 = sys_fork()
255 if pid != 0 {
256 // parent: report the daemon pid and exit
257 let line: *u8 = sys_mmap(128)
258 var o: i64 = 0
259 o = gl_cat(line, o, "GATELOOP daemonized pid=" as *u8)
260 o = gl_cat_n(line, o, pid)
261 o = gl_cat(line, o, " beat_ms=" as *u8)
262 o = gl_cat_n(line, o, beat_ms)
263 o = gl_cat(line, o, "\n" as *u8)
264 sys_write(1, line, o)
265 return 0
266 }
267 nx_setsid()
268 // lock is taken by the DAEMON child (its own pid), after setsid
269 if gl_acquire_lock() == 0 { return 3 }
270 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
271 if devnull >= 0 {
272 sys_dup3(devnull, 0, 0)
273 sys_dup3(devnull, 1, 0)
274 sys_dup3(devnull, 2, 0)
275 }
276 gl_loop(beat_ms, 0)
277 return 0
278}