code wiki / _hdl_build / nx_worklog_ingest_lib.nx

nx_worklog_ingest_lib.nx source

↩ module page · 347 lines · 15518 B

1// nx_worklog_ingest_lib.nx -- the PRESENT-axis workhorse: ingest the session transcript (.jsonl the 2// harness writes out-of-band as work happens) into the append-only work-journal. A byte-offset 3// CURSOR makes it idempotent: each run appends one framed line per NEW tool_use since the cursor, 4// then advances the cursor to the last complete line. Idempotence is the key property -- the SAME 5// organ can fire on PostToolUse (real-time / mid-turn crash durability), on Stop (turn end), and on a 6// Cron timer (fully out-of-band redundancy) with ZERO duplicate lines, because the cursor gates them 7// all. Reading the harness's OWN record means capture can never miss an action. 8// 9// REUSE: getdents64 + fstatat(mtime@88) directory walk is the proven nx_ws_index_lib/nx_sov_tree_audit 10// idiom; line-boundary cursor is the standard JSONL tail-follow; atomic cursor write = tmp+renameat. 11// Sovereign: nx_syscalls + nx_worklog_lib + nx_json_lib only. license_tier: ORIGINAL 12import "nx_syscalls.nx" 13import "nx_worklog_lib.nx" 14import "nx_json_lib.nx" 15const WLI_MAGIC_1024: i64 = 1024 16const WLI_MAGIC_65536: i64 = 65536 17const WLI_MAGIC_2048: i64 = 2048 18const WLI_MAGIC_131072: i64 = 131072 19 20const WLI_READCAP: i64 = 8388608 // 8MB per ingest pass (process up to last newline within it) 21const WLI_TOOLCAP: i64 = 64 22const WLI_TGTCAP: i64 = 256 23const WLI_TGTWIN: i64 = 4096 // bound the first-string-value search inside an input object 24 25func wli_cat(dst: *u8, off: i64, s: *u8) -> i64 { 26 var o: i64 = off 27 var i: i64 = 0 28 while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } 29 return o 30} 31func wli_catn(dst: *u8, off: i64, v: i64) -> i64 { 32 var o: i64 = off 33 var m: i64 = v 34 if m == 0 { dst[o] = 48 as u8; return o + 1 } 35 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 36 let t: *u8 = sys_mmap(32) 37 var k: i64 = 0 38 while m > 0 { t[k] = ((48 + (m % 10)) as u8); m = m / 10; k = k + 1 } 39 while k > 0 { dst[o] = t[k - 1]; o = o + 1; k = k - 1 } 40 return o 41} 42 43// read a decimal byte-offset cursor; absent/empty/garbage -> 0 (start from the top, safe). 44func wli_read_cursor(cursor_path: *u8) -> i64 { 45 let fd: i64 = sys_openat_rd(cursor_path) 46 if fd < 0 { return 0 } 47 let buf: *u8 = sys_mmap(64) 48 let nrd: i64 = sys_read(fd, buf, 63) 49 sys_close(fd) 50 if nrd <= 0 { return 0 } 51 var v: i64 = 0 52 var i: i64 = 0 53 while i < nrd { 54 let c: i64 = buf[i] as i64 55 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 56 i = i + 1 57 } 58 return v 59} 60 61// write the cursor atomically (tmp + renameat) so a crash mid-write never yields a torn offset. 62func wli_write_cursor(cursor_path: *u8, v: i64) -> i64 { 63 let buf: *u8 = sys_mmap(64) 64 var o: i64 = wli_catn(buf, 0, v) 65 buf[o] = 10 as u8; o = o + 1 66 let tmp: *u8 = sys_mmap(WLI_MAGIC_1024) 67 var t: i64 = wli_cat(tmp, 0, cursor_path) 68 let suf: *u8 = ".tmp" as *u8 69 t = wli_cat(tmp, t, suf) 70 tmp[t] = 0 as u8 71 let fd: i64 = sys_openat_wr(tmp, 420) 72 if fd < 0 { return 0 - 1 } 73 sys_write(fd, buf, o) 74 sys_close(fd) 75 sys_renameat(tmp, cursor_path) 76 return 0 77} 78 79// per-transcript cursor path: <WL_DIR>/cursors/<basename(transcript)>.cur -- so distinct sessions 80// never share an offset (which would corrupt capture across a restart). returns length. 81func wli_default_cursor(transcript: *u8, out: *u8, outcap: i64) -> i64 { 82 let tl: i64 = jx_len(transcript) 83 var bstart: i64 = 0 84 var i: i64 = 0 85 while i < tl { if transcript[i] == (47 as u8) { bstart = i + 1 } i = i + 1 } 86 var o: i64 = wli_cat(out, 0, WL_DIR) // const passed as arg = OK 87 let mid: *u8 = "/cursors/" as *u8 88 o = wli_cat(out, o, mid) 89 var j: i64 = bstart 90 while j < tl { out[o] = transcript[j]; o = o + 1; j = j + 1 } 91 let suf: *u8 = ".cur" as *u8 92 o = wli_cat(out, o, suf) 93 out[o] = 0 as u8 94 return o 95} 96 97// THE INGEST: append one "ingest" work-journal line per NEW tool_use since the cursor; advance cursor. 98// returns the number of newly-captured actions (0 if none / no transcript yet -- always graceful). 99func wli_ingest(transcript: *u8, cursor_path: *u8, worklog: *u8) -> i64 { 100 let start: i64 = wli_read_cursor(cursor_path) 101 let fd: i64 = sys_openat_rd(transcript) 102 if fd < 0 { return 0 } // no transcript yet -> nothing, no error 103 sys_lseek(fd, start, 0) // SEEK_SET 104 let buf: *u8 = sys_mmap(WLI_READCAP) 105 var total: i64 = 0 106 var nrd: i64 = sys_read(fd, buf, WLI_READCAP) 107 while nrd > 0 { 108 total = total + nrd 109 if total >= WLI_READCAP { nrd = 0 } else { nrd = sys_read(fd, ((buf as i64) + total) as *u8, WLI_READCAP - total) } 110 } 111 sys_close(fd) 112 if total <= 0 { return 0 } 113 114 // only process COMPLETE lines: trim to the last newline so no tool_use ever spans the cursor. 115 var lastnl: i64 = 0 - 1 116 var p: i64 = total - 1 117 while p >= 0 { if buf[p] == (10 as u8) { lastnl = p; p = 0 - 1 } else { p = p - 1 } } 118 if lastnl < 0 { return 0 } 119 let limit: i64 = lastnl + 1 120 121 let anchor: *u8 = "\"type\":\"tool_use\"" as *u8 122 let namekey: *u8 = "name" as *u8 123 let inputpat: *u8 = "\"input\":{" as *u8 124 let colq: *u8 = ":\"" as *u8 125 let inlen: i64 = jx_len(inputpat) 126 127 var count: i64 = 0 128 var at: i64 = jx_find(buf, limit, 0, anchor) 129 while at >= 0 { 130 let tool: *u8 = sys_mmap(WLI_TOOLCAP) 131 tool[0] = 45 as u8; tool[1] = 0 as u8 // default "-" 132 let tgt: *u8 = sys_mmap(WLI_TGTCAP) 133 tgt[0] = 45 as u8; tgt[1] = 0 as u8 // default "-" 134 let after_name: i64 = jx_get_str(buf, limit, at, namekey, tool, WLI_TOOLCAP) 135 var nexti: i64 = at + 16 136 if after_name >= 0 { 137 nexti = after_name 138 let inpos: i64 = jx_find(buf, limit, after_name, inputpat) 139 if inpos >= 0 { 140 var win: i64 = inpos + WLI_TGTWIN 141 if win > limit { win = limit } 142 let vq: i64 = jx_find(buf, win, inpos + inlen, colq) 143 if vq >= 0 { jx_copy_str(buf, limit, vq + 2, tgt, WLI_TGTCAP) } 144 } 145 wl_append3(worklog, "ingest" as *u8, tool, tgt) 146 count = count + 1 147 } 148 at = jx_find(buf, limit, nexti, anchor) 149 } 150 wli_write_cursor(cursor_path, start + limit) 151 return count 152} 153 154// translate a Windows path ("C:\Users\...") to its WSL form ("/mnt/c/Users/..."). Hook stdin reports 155// Windows paths but the organ runs under WSL. Already-POSIX (leading '/') paths copy through. The 156// JSON de-escape already collapsed "\\" -> "\", so here we only fold drive-letter + backslashes. 157func wli_winpath_to_wsl(src: *u8, out: *u8) -> i64 { 158 let n: i64 = jx_len(src) 159 if n == 0 { out[0] = 0 as u8; return 0 } 160 if src[0] == (47 as u8) { // already POSIX 161 var i: i64 = 0 162 while src[i] != (0 as u8) { out[i] = src[i]; i = i + 1 } 163 out[i] = 0 as u8 164 return i 165 } 166 if n >= 2 { if src[1] == (58 as u8) { // "<drive>:" form 167 let mnt: *u8 = "/mnt/" as *u8 168 var o: i64 = wli_cat(out, 0, mnt) 169 var dl: i64 = src[0] as i64 170 if dl >= 65 { if dl <= 90 { dl = dl + 32 } } // drive letter upper -> lower 171 out[o] = dl as u8; o = o + 1 172 var i: i64 = 2 173 while i < n { 174 var c: i64 = src[i] as i64 175 if c == 92 { c = 47 } // backslash -> slash 176 out[o] = c as u8; o = o + 1 177 i = i + 1 178 } 179 out[o] = 0 as u8 180 return o 181 } } 182 var j: i64 = 0 // unknown shape: copy verbatim 183 while src[j] != (0 as u8) { out[j] = src[j]; j = j + 1 } 184 out[j] = 0 as u8 185 return j 186} 187 188// ".jsonl" suffix test (for the cron newest-transcript fallback). 189func wli_is_jsonl(name: *u8) -> i64 { 190 let n: i64 = jx_len(name) 191 if n < 7 { return 0 } 192 let suf: *u8 = ".jsonl" as *u8 193 var k: i64 = 0 194 while k < 6 { if name[n - 6 + k] != suf[k] { return 0 } k = k + 1 } 195 return 1 196} 197 198// newest *.jsonl in `dir` (highest st_mtim.tv_sec) -> out. returns 0 if found, -1 if none/unopenable. 199// lets a Cron timer run with no args and still target the live session. getdents64 + fstatat idiom. 200func wli_newest_transcript(dir: *u8, out: *u8, outcap: i64) -> i64 { 201 let fd: i64 = sys_openat_rd(dir) 202 if fd < 0 { return 0 - 1 } 203 let gbuf: *u8 = sys_mmap(WLI_MAGIC_65536) 204 let stbuf: *u8 = sys_mmap(256) 205 let child: *u8 = sys_mmap(WLI_MAGIC_2048) 206 var best_mt: i64 = 0 - 1 207 var found: i64 = 0 208 var nread: i64 = __syscall(217, fd, gbuf, WLI_MAGIC_65536, 0, 0, 0) 209 while nread > 0 { 210 var off: i64 = 0 211 while off < nread { 212 let reclen: i64 = (gbuf[off + 16] as i64) | ((gbuf[off + 17] as i64) << 8) 213 if reclen <= 0 { off = nread } else { 214 let name: *u8 = ((gbuf as i64) + off + 19) as *u8 215 if wli_is_jsonl(name) == 1 { 216 var co: i64 = wli_cat(child, 0, dir) 217 child[co] = 47 as u8; co = co + 1 218 var ci: i64 = 0 219 while name[ci] != (0 as u8) { child[co] = name[ci]; co = co + 1; ci = ci + 1 } 220 child[co] = 0 as u8 221 var mt: i64 = 0 222 if sys_fstatat(child, stbuf) == 0 { 223 mt = (stbuf[88] as i64) | ((stbuf[89] as i64) << 8) | ((stbuf[90] as i64) << 16) | ((stbuf[91] as i64) << 24) | ((stbuf[92] as i64) << 32) | ((stbuf[93] as i64) << 40) 224 } 225 if mt > best_mt { 226 best_mt = mt 227 var oo: i64 = 0 228 while child[oo] != (0 as u8) { out[oo] = child[oo]; oo = oo + 1 } 229 out[oo] = 0 as u8 230 found = 1 231 } 232 } 233 off = off + reclen 234 } 235 } 236 nread = __syscall(217, fd, gbuf, WLI_MAGIC_65536, 0, 0, 0) 237 } 238 sys_close(fd) 239 if found == 1 { return 0 } 240 return 0 - 1 241} 242 243// ONE capture entry point shared by every runner/hook (DRY): acquire the transcript by precedence 244// explicit path (argpath != 0) > hook stdin "transcript_path" > newest *.jsonl in projdir 245// (Windows paths translated to WSL), compute the per-transcript cursor, ingest. returns lines added. 246// Graceful: no transcript anywhere -> 0, never errors. Caller must NOT also read stdin separately. 247// Placed last so every callee is already defined above (definition-before-use). 248func wli_capture(argpath: *u8, projdir: *u8, cursors_dir: *u8) -> i64 { 249 wl_ensure_dir(WL_DIR) 250 sys_mkdir(cursors_dir, 511) 251 let transcript: *u8 = sys_mmap(WLI_MAGIC_2048) 252 var have: i64 = 0 253 if (argpath as i64) != 0 { 254 wli_winpath_to_wsl(argpath, transcript) 255 have = 1 256 } 257 if have == 0 { 258 let sbuf: *u8 = sys_mmap(WLI_MAGIC_131072) 259 var sn: i64 = 0 260 var r: i64 = sys_read(0, sbuf, WLI_MAGIC_131072) 261 while r > 0 { 262 sn = sn + r 263 if sn >= WLI_MAGIC_131072 { r = 0 } else { r = sys_read(0, ((sbuf as i64) + sn) as *u8, WLI_MAGIC_131072 - sn) } 264 } 265 if sn > 0 { 266 let raw: *u8 = sys_mmap(WLI_MAGIC_2048) 267 let key: *u8 = "transcript_path" as *u8 268 if jx_get_str(sbuf, sn, 0, key, raw, WLI_MAGIC_2048) >= 0 { 269 wli_winpath_to_wsl(raw, transcript) 270 have = 1 271 } 272 } 273 } 274 if have == 0 { 275 if wli_newest_transcript(projdir, transcript, WLI_MAGIC_2048) == 0 { have = 1 } 276 } 277 if have == 0 { return 0 } 278 let cursor: *u8 = sys_mmap(WLI_MAGIC_2048) 279 wli_default_cursor(transcript, cursor, WLI_MAGIC_2048) 280 return wli_ingest(transcript, cursor, WL_PATH) 281} 282 283// ".cur" suffix test (cursor files are named "<transcript_basename>.cur"). 284func wli_is_cur(name: *u8) -> i64 { 285 let n: i64 = jx_len(name) 286 if n < 5 { return 0 } 287 let suf: *u8 = ".cur" as *u8 288 var k: i64 = 0 289 while k < 4 { if name[n - 4 + k] != suf[k] { return 0 } k = k + 1 } 290 return 1 291} 292 293// CRASH-HEAL SWEEP -- the missing crash-durability piece. Re-ingest the stranded tail of EVERY session 294// whose cursor sits BEHIND its transcript: that gap is the exact signature of a session that WAS being 295// captured but never got a final Stop hook (i.e. it CRASHED). Driven by the CURSORS dir -- one .cur per 296// session the worklog already owns -- NOT the transcript dir, so (a) pre-worklog transcripts are never 297// touched and (b) cost is one stat per known session. Idempotent: a cleanly-finished session has 298// cursor==size, so its ingest is a no-op (0 new lines, zero duplicates); that same idempotence makes a 299// transient re-visit during the dir walk harmless. After ANY crash, the next session's first Stop calls 300// this and the crashed tail is captured -- closing the hole BY CONSTRUCTION. returns lines healed. 301func wli_sweep_behind_to(projdir: *u8, cursors_dir: *u8, worklog: *u8) -> i64 { 302 let fd: i64 = sys_openat_rd(cursors_dir) 303 if fd < 0 { return 0 } // no cursors yet -> nothing, graceful 304 let gbuf: *u8 = sys_mmap(WLI_MAGIC_65536) 305 let stbuf: *u8 = sys_mmap(256) 306 let cpath: *u8 = sys_mmap(WLI_MAGIC_2048) 307 let tpath: *u8 = sys_mmap(WLI_MAGIC_2048) 308 var recovered: i64 = 0 309 var nread: i64 = __syscall(217, fd, gbuf, WLI_MAGIC_65536, 0, 0, 0) 310 while nread > 0 { 311 var off: i64 = 0 312 while off < nread { 313 let reclen: i64 = (gbuf[off + 16] as i64) | ((gbuf[off + 17] as i64) << 8) 314 if reclen <= 0 { off = nread } else { 315 let name: *u8 = ((gbuf as i64) + off + 19) as *u8 316 if wli_is_cur(name) == 1 { 317 var co: i64 = wli_cat(cpath, 0, cursors_dir) // cursor path = cursors_dir/name 318 cpath[co] = 47 as u8; co = co + 1 319 var ci: i64 = 0 320 while name[ci] != (0 as u8) { cpath[co] = name[ci]; co = co + 1; ci = ci + 1 } 321 cpath[co] = 0 as u8 322 var to: i64 = wli_cat(tpath, 0, projdir) // transcript = projdir/(name minus ".cur") 323 tpath[to] = 47 as u8; to = to + 1 324 let blen: i64 = ci - 4 // ci = strlen(name); strip ".cur" 325 var ti: i64 = 0 326 while ti < blen { tpath[to] = name[ti]; to = to + 1; ti = ti + 1 } 327 tpath[to] = 0 as u8 328 var sz: i64 = 0 - 1 // size > cursor => un-ingested tail (a crash) 329 if sys_fstatat(tpath, stbuf) == 0 { 330 sz = (stbuf[48] as i64) | ((stbuf[49] as i64) << 8) | ((stbuf[50] as i64) << 16) | ((stbuf[51] as i64) << 24) | ((stbuf[52] as i64) << 32) | ((stbuf[53] as i64) << 40) 331 } 332 let cur: i64 = wli_read_cursor(cpath) 333 if sz > cur { recovered = recovered + wli_ingest(tpath, cpath, worklog) } 334 } 335 off = off + reclen 336 } 337 } 338 nread = __syscall(217, fd, gbuf, WLI_MAGIC_65536, 0, 0, 0) 339 } 340 sys_close(fd) 341 return recovered 342} 343 344// convenience: heal into the real work-journal (WL_PATH). Called by the Stop-hook / cron runner. 345func wli_sweep_behind(projdir: *u8, cursors_dir: *u8) -> i64 { 346 return wli_sweep_behind_to(projdir, cursors_dir, WL_PATH) 347}