code wiki / (root) / nx_nxtask_store.nx

nx_nxtask_store.nx source

↩ module page · 367 lines · 11867 B

1// nxtask_store.nx -- persistence layer for nxtask (beads-equivalent). 2// 3// Provides buffer round-trip of a TaskGraph + convenience wrappers 4// that go through fs.nx for disk persistence. Single-file binary 5// format; zero runtime deps beyond syscalls + fs. 6// 7// Format v1 -- magic "NXT0": 8// 9// 4 magic ("NXT0", little-endian bytes) 10// 8 n_tasks (i64 LE) 11// repeat n_tasks: 12// 8 state 13// 8 created_ns 14// 8 updated_ns 15// 8 desc_len 16// desc_len desc bytes (no trailing NUL) 17// 8 claimed_by_len 18// claimed_by_len claimed_by bytes (omitted when 0) 19// 8 summary_len 20// summary_len summary bytes (omitted when 0) 21// 8 n_blocked 22// 8*n_blocked blocked_by task indices (i64 LE each) 23// 24// Design notes: 25// 26// * We store strings inline rather than via a separate string pool 27// because each task's text is small (<2 KB) and the whole file 28// typically fits in one page. A pool only pays off when many 29// tasks share identical text, which isn't the nxtask workload. 30// 31// * id_hash is NOT persisted. v0.0.1 uses counter-hash which is a 32// function of the task's position + creation order, both of 33// which are recovered at load time. v0.1.0 will persist a real 34// sha256(desc + created_ns) once we switch to content-addressed 35// ids. 36// 37// * Little-endian is the RISC-V default so this is the native byte 38// order. No byte-swap needed. 39// 40// This commit ships the buffer API only. An fs_save/fs_load wrapper 41// lands in a follow-up (depends on fs_mkdir parent-dir creation). 42 43// nx_safety_envelope: 44// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 45// sil_target: SIL1 46// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 47// verdict: NOT_YET_EVALUATED 48 49import "nx_syscalls.nx" 50import "nx_nxtask.nx" 51const NXT_MAGIC_16384: i64 = 16384 52 53const NXT_MAGIC_0: i64 = 0x4E // 'N' 54const NXT_MAGIC_1: i64 = 0x58 // 'X' 55const NXT_MAGIC_2: i64 = 0x54 // 'T' 56const NXT_MAGIC_3: i64 = 0x30 // '0' 57 58// === helpers: i64 <-> bytes (little-endian) ======================== 59 60func write_i64_le(buf: *u8, off: i64, val: i64) -> i64 { 61 var i: i64 = 0 62 while i < 8 { 63 buf[off + i] = (val >> (i * 8)) & 0xFF 64 i = i + 1 65 } 66 return off + 8 67} 68 69func read_i64_le(buf: *u8, off: i64) -> i64 { 70 var v: i64 = 0 71 var i: i64 = 0 72 while i < 8 { 73 let b: i64 = buf[off + i] & 0xFF 74 v = v | (b << (i * 8)) 75 i = i + 1 76 } 77 return v 78} 79 80// Copy `n` bytes from `src` to `dst`. Returns new dst offset. 81func copy_bytes(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { 82 var i: i64 = 0 83 while i < n { 84 dst[off + i] = src[i] 85 i = i + 1 86 } 87 return off + n 88} 89 90// === serialize ===================================================== 91// 92// Write a graph to `buf` (must be >= nxtask_serialized_size). 93// Returns bytes written, or -1 on overflow. 94 95func nxtask_serialized_size(g: *TaskGraph) -> i64 { 96 var total: i64 = 4 + 8 // magic + n_tasks 97 var i: i64 = 0 98 while i < g.n_tasks { 99 let t: *Task = task_at(g, i) 100 total = total + 8 + 8 + 8 // state + created + updated 101 total = total + 8 + t.desc_len 102 total = total + 8 + t.claimed_by_len 103 total = total + 8 + t.summary_len 104 total = total + 8 + t.n_blocked * 8 105 i = i + 1 106 } 107 return total 108} 109 110func nxtask_serialize(g: *TaskGraph, buf: *u8, cap: i64) -> i64 { 111 let need: i64 = nxtask_serialized_size(g) 112 if need > cap { return -1 } 113 114 // Magic 115 buf[0] = NXT_MAGIC_0 & 0xFF 116 buf[1] = NXT_MAGIC_1 & 0xFF 117 buf[2] = NXT_MAGIC_2 & 0xFF 118 buf[3] = NXT_MAGIC_3 & 0xFF 119 var off: i64 = 4 120 off = write_i64_le(buf, off, g.n_tasks) 121 122 var i: i64 = 0 123 while i < g.n_tasks { 124 let t: *Task = task_at(g, i) 125 off = write_i64_le(buf, off, t.state) 126 off = write_i64_le(buf, off, t.created_ns) 127 off = write_i64_le(buf, off, t.updated_ns) 128 off = write_i64_le(buf, off, t.desc_len) 129 if t.desc_len > 0 { 130 off = copy_bytes(buf, off, t.desc, t.desc_len) 131 } 132 off = write_i64_le(buf, off, t.claimed_by_len) 133 if t.claimed_by_len > 0 { 134 off = copy_bytes(buf, off, t.claimed_by, t.claimed_by_len) 135 } 136 off = write_i64_le(buf, off, t.summary_len) 137 if t.summary_len > 0 { 138 off = copy_bytes(buf, off, t.summary, t.summary_len) 139 } 140 off = write_i64_le(buf, off, t.n_blocked) 141 var j: i64 = 0 142 while j < t.n_blocked { 143 off = write_i64_le(buf, off, t.blocked_by[j]) 144 j = j + 1 145 } 146 i = i + 1 147 } 148 return off 149} 150 151// === deserialize =================================================== 152// 153// Read from a buf of size `n`. Returns 0 (null) on malformed input. 154 155func nxtask_deserialize(buf: *u8, n: i64) -> *TaskGraph { 156 if n < 12 { return 0 as *TaskGraph } // magic + header 157 if buf[0] != NXT_MAGIC_0 { return 0 as *TaskGraph } 158 if buf[1] != NXT_MAGIC_1 { return 0 as *TaskGraph } 159 if buf[2] != NXT_MAGIC_2 { return 0 as *TaskGraph } 160 if buf[3] != NXT_MAGIC_3 { return 0 as *TaskGraph } 161 162 var off: i64 = 4 163 let n_tasks: i64 = read_i64_le(buf, off) 164 off = off + 8 165 if n_tasks < 0 { return 0 as *TaskGraph } 166 167 let cap: i64 = n_tasks + 32 // grow headroom 168 let g: *TaskGraph = nxtask_graph_new(cap) 169 g.n_tasks = n_tasks 170 171 var i: i64 = 0 172 while i < n_tasks { 173 if off + 32 > n { return 0 as *TaskGraph } // truncated 174 let t: *Task = task_at(g, i) 175 t.state = read_i64_le(buf, off) 176 off = off + 8 177 t.created_ns = read_i64_le(buf, off) 178 off = off + 8 179 t.updated_ns = read_i64_le(buf, off) 180 off = off + 8 181 182 // desc 183 let d_len: i64 = read_i64_le(buf, off) 184 off = off + 8 185 if d_len < 0 { return 0 as *TaskGraph } 186 if off + d_len > n { return 0 as *TaskGraph } 187 t.desc_len = d_len 188 if d_len > 0 { 189 let d: *u8 = sys_mmap(d_len + 16) 190 var k: i64 = 0 191 while k < d_len { 192 d[k] = buf[off + k] 193 k = k + 1 194 } 195 d[d_len] = 0 196 t.desc = d 197 off = off + d_len 198 } else { 199 t.desc = 0 as *u8 200 } 201 202 // claimed_by 203 let c_len: i64 = read_i64_le(buf, off) 204 off = off + 8 205 if c_len < 0 { return 0 as *TaskGraph } 206 if off + c_len > n { return 0 as *TaskGraph } 207 t.claimed_by_len = c_len 208 if c_len > 0 { 209 let c: *u8 = sys_mmap(c_len + 16) 210 var k: i64 = 0 211 while k < c_len { 212 c[k] = buf[off + k] 213 k = k + 1 214 } 215 c[c_len] = 0 216 t.claimed_by = c 217 off = off + c_len 218 } else { 219 t.claimed_by = 0 as *u8 220 } 221 222 // summary 223 let s_len: i64 = read_i64_le(buf, off) 224 off = off + 8 225 if s_len < 0 { return 0 as *TaskGraph } 226 if off + s_len > n { return 0 as *TaskGraph } 227 t.summary_len = s_len 228 if s_len > 0 { 229 let s: *u8 = sys_mmap(s_len + 16) 230 var k: i64 = 0 231 while k < s_len { 232 s[k] = buf[off + k] 233 k = k + 1 234 } 235 s[s_len] = 0 236 t.summary = s 237 off = off + s_len 238 } else { 239 t.summary = 0 as *u8 240 } 241 242 // n_blocked + blocked_by 243 let nb: i64 = read_i64_le(buf, off) 244 off = off + 8 245 if nb < 0 { return 0 as *TaskGraph } 246 if off + nb * 8 > n { return 0 as *TaskGraph } 247 t.n_blocked = nb 248 if nb > 0 { 249 let deps: *i64 = sys_mmap(nb * 8 + 16) as *i64 250 var k: i64 = 0 251 while k < nb { 252 deps[k] = read_i64_le(buf, off) 253 off = off + 8 254 k = k + 1 255 } 256 t.blocked_by = deps 257 } else { 258 t.blocked_by = 0 as *i64 259 } 260 261 i = i + 1 262 } 263 264 return g 265} 266 267// === self-test ===================================================== 268// 269// Round-trip 3 tasks with mixed state + deps. Assert every field 270// survives. Exit 42 on success, other codes on specific mismatches. 271 272func assert_bytes_equal(a: *u8, b: *u8, n: i64) -> i64 { 273 var i: i64 = 0 274 while i < n { 275 if a[i] != b[i] { return 0 } 276 i = i + 1 277 } 278 return 1 279} 280 281func main() -> i64 { 282 // Build source graph: 3 tasks, mid-workflow state. 283 let g: *TaskGraph = nxtask_graph_new(8) 284 nxtask_add(g, "Port 12-bit imm expansion" as *u8, 25, 1) 285 nxtask_add(g, "Add persistence to nxtask" as *u8, 25, 2) 286 nxtask_add(g, "Build Mormon v0" as *u8, 15, 3) 287 288 nxtask_add_dep(g, 1, 0) // persistence blocked by imm port 289 nxtask_add_dep(g, 2, 1) // mormon blocked by persistence 290 291 nxtask_claim(g, 0, "agent-foo" as *u8, 9, 10) 292 nxtask_close(g, 0, "Shipped with four helpers" as *u8, 25, 11) 293 294 // Serialize. 295 let buf: *u8 = sys_mmap(NXT_MAGIC_16384) 296 let n: i64 = nxtask_serialize(g, buf, NXT_MAGIC_16384) 297 if n < 0 { return __syscall(93, 80, 0, 0, 0, 0, 0) } 298 if n < 12 { return __syscall(93, 81, 0, 0, 0, 0, 0) } 299 300 // Magic bytes at offset 0-3. 301 if buf[0] != 0x4E { return __syscall(93, 82, 0, 0, 0, 0, 0) } 302 if buf[1] != 0x58 { return __syscall(93, 83, 0, 0, 0, 0, 0) } 303 if buf[2] != 0x54 { return __syscall(93, 84, 0, 0, 0, 0, 0) } 304 if buf[3] != 0x30 { return __syscall(93, 85, 0, 0, 0, 0, 0) } 305 306 // Deserialize. 307 let g2: *TaskGraph = nxtask_deserialize(buf, n) 308 if g2 == (0 as *TaskGraph) { return __syscall(93, 86, 0, 0, 0, 0, 0) } 309 if g2.n_tasks != 3 { return __syscall(93, 87, 0, 0, 0, 0, 0) } 310 311 // Per-task field round-trip. 312 let t0: *Task = task_at(g, 0) 313 let u0: *Task = task_at(g2, 0) 314 if u0.state != t0.state { return __syscall(93, 90, 0, 0, 0, 0, 0) } 315 if u0.created_ns != t0.created_ns { return __syscall(93, 91, 0, 0, 0, 0, 0) } 316 if u0.updated_ns != t0.updated_ns { return __syscall(93, 92, 0, 0, 0, 0, 0) } 317 if u0.desc_len != t0.desc_len { return __syscall(93, 93, 0, 0, 0, 0, 0) } 318 if assert_bytes_equal(u0.desc, t0.desc, t0.desc_len) != 1 { 319 return __syscall(93, 94, 0, 0, 0, 0, 0) 320 } 321 if u0.claimed_by_len != t0.claimed_by_len { 322 return __syscall(93, 95, 0, 0, 0, 0, 0) 323 } 324 if assert_bytes_equal(u0.claimed_by, t0.claimed_by, t0.claimed_by_len) != 1 { 325 return __syscall(93, 96, 0, 0, 0, 0, 0) 326 } 327 if u0.summary_len != t0.summary_len { 328 return __syscall(93, 97, 0, 0, 0, 0, 0) 329 } 330 if assert_bytes_equal(u0.summary, t0.summary, t0.summary_len) != 1 { 331 return __syscall(93, 98, 0, 0, 0, 0, 0) 332 } 333 334 // Dep round-trip on task 1. 335 let t1: *Task = task_at(g, 1) 336 let u1: *Task = task_at(g2, 1) 337 if u1.n_blocked != t1.n_blocked { 338 return __syscall(93, 100, 0, 0, 0, 0, 0) 339 } 340 if u1.blocked_by[0] != 0 { 341 return __syscall(93, 101, 0, 0, 0, 0, 0) 342 } 343 344 // Task 2 (never claimed) -- claimed_by + summary should be null/zero. 345 let u2: *Task = task_at(g2, 2) 346 if u2.claimed_by_len != 0 { 347 return __syscall(93, 110, 0, 0, 0, 0, 0) 348 } 349 if u2.summary_len != 0 { 350 return __syscall(93, 111, 0, 0, 0, 0, 0) 351 } 352 if u2.n_blocked != 1 { 353 return __syscall(93, 112, 0, 0, 0, 0, 0) 354 } 355 if u2.blocked_by[0] != 1 { 356 return __syscall(93, 113, 0, 0, 0, 0, 0) 357 } 358 359 // Malformed-magic deserialise path returns null. 360 buf[0] = 0x00 361 let g_bad: *TaskGraph = nxtask_deserialize(buf, n) 362 if g_bad != (0 as *TaskGraph) { 363 return __syscall(93, 120, 0, 0, 0, 0, 0) 364 } 365 366 return __syscall(93, 42, 0, 0, 0, 0, 0) 367}