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}