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}