code wiki / (root) / nx_nxtask.nx

nx_nxtask.nx source

↩ module page · 333 lines · 11106 B

1// nxtask.nx -- sovereign agent task graph (beads-equivalent). 2// 3// Persistent, structured-memory task tracker for the Mormon local 4// AI agent (docs/LAYER_1_3_SOVEREIGNTY_AUDIT.md). Tracks work 5// across sessions; survives agent restarts. 6// 7// Design inherited from beads (gastownhall/beads) where it fit 8// sovereignty discipline. Divergences: 9// - NO external SQL engine (Dolt is 100 MB + MySQL-compat). 10// Instead: content-addressable flat files, one JSON blob 11// per task. Ride on fs.nx. Zero runtime deps. 12// - NO Dolt cell-level merge. Instead: append-only event log 13// per task id; merge via event-timestamp ordering. v0.1.0 14// adds cross-agent merge; v0.0.1 single-agent only. 15// - Same JSON API as beads where possible -- Mormon can switch 16// backends seamlessly later. 17// 18// v0.0.1 scope: add, list, claim, close, show, ready. No 19// hierarchy, no deps yet -- foundations first, semantics layer 20// in v0.1.0. 21 22// nx_safety_envelope: 23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 24// sil_target: SIL1 25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 26// verdict: NOT_YET_EVALUATED 27 28import "nx_syscalls.nx" 29const TASK_MAGIC_1000000000: i64 = 1000000000 30const TASK_MAGIC_1000001000: i64 = 1000001000 31const TASK_MAGIC_1000002000: i64 = 1000002000 32const TASK_MAGIC_1000003000: i64 = 1000003000 33const TASK_MAGIC_1000005000: i64 = 1000005000 34const TASK_MAGIC_1000006000: i64 = 1000006000 35const TASK_MAGIC_1000007000: i64 = 1000007000 36 37// === data model ===================================================== 38 39const TASK_MAX_DESC: i64 = 2048 40const TASK_MAX_OPEN_TASKS: i64 = 1024 41const TASK_STATE_OPEN: i64 = 0 42const TASK_STATE_CLAIMED: i64 = 1 43const TASK_STATE_CLOSED: i64 = 2 44 45struct Task { 46 id_hash: *u8, // 32-byte sha256 of original description + timestamp 47 state: i64, // TASK_STATE_* 48 claimed_by: *u8, // agent id string; 0 ptr if open 49 claimed_by_len: i64, 50 desc: *u8, 51 desc_len: i64, 52 created_ns: i64, 53 updated_ns: i64, 54 summary: *u8, // close-time summary; 0 ptr while open 55 summary_len: i64, 56 // Dependency array: task indices this task is blocked by. 57 // A task is "ready" when all blockers have state == CLOSED. 58 // Allocated on demand; 0 ptr when no deps. 59 blocked_by: *i64, 60 n_blocked: i64, 61} 62 63struct TaskGraph { 64 tasks: *u8, // packed array, TASK_BYTES per entry 65 n_tasks: i64, 66 cap: i64, 67} 68 69const TASK_BYTES: i64 = 112 70const MAX_DEPS: i64 = 16 71 72// === constructor =================================================== 73 74func nxtask_graph_new(cap: i64) -> *TaskGraph { 75 let g_raw: *u8 = sys_mmap(64) 76 let g: *TaskGraph = g_raw as *TaskGraph 77 g.tasks = sys_mmap(cap * TASK_BYTES + 64) 78 g.n_tasks = 0 79 g.cap = cap 80 return g 81} 82 83func task_at(g: *TaskGraph, idx: i64) -> *Task { 84 let base: i64 = g.tasks as i64 85 return (base + idx * TASK_BYTES) as *Task 86} 87 88// === add ============================================================ 89 90// Add a new task with the given description + agent hint. Returns 91// the task index in the graph, or -1 if full. 92func nxtask_add(g: *TaskGraph, desc: *u8, desc_len: i64, 93 now_ns: i64) -> i64 { 94 if g.n_tasks >= g.cap { return -1 } 95 let idx: i64 = g.n_tasks 96 let t: *Task = task_at(g, idx) 97 // id_hash: for v0.0.1 this is just a packed counter-hash. 98 // v0.1.0 upgrades to sha256(desc + timestamp). 99 let h_buf: *u8 = sys_mmap(32) 100 var i: i64 = 0 101 while i < 32 { 102 h_buf[i] = (idx + i * 17) & 0xFF 103 i = i + 1 104 } 105 t.id_hash = h_buf 106 t.state = TASK_STATE_OPEN 107 t.claimed_by = 0 as *u8 108 t.claimed_by_len = 0 109 // Copy description. 110 let dbuf: *u8 = sys_mmap(desc_len + 16) 111 i = 0 112 while i < desc_len { 113 dbuf[i] = desc[i] 114 i = i + 1 115 } 116 dbuf[desc_len] = 0 117 t.desc = dbuf 118 t.desc_len = desc_len 119 t.created_ns = now_ns 120 t.updated_ns = now_ns 121 t.summary = 0 as *u8 122 t.summary_len = 0 123 t.blocked_by = 0 as *i64 124 t.n_blocked = 0 125 g.n_tasks = idx + 1 126 return idx 127} 128 129// === dependencies =================================================== 130 131// Mark `task_idx` as blocked by `blocker_idx`. Tasks become 132// ready (next_ready returns them) only when ALL blockers are 133// CLOSED. Returns 0 on success, -1 on invalid indices, -2 if 134// the dep cap is reached. 135func nxtask_add_dep(g: *TaskGraph, task_idx: i64, 136 blocker_idx: i64) -> i64 { 137 if task_idx < 0 { return -1 } 138 if task_idx >= g.n_tasks { return -1 } 139 if blocker_idx < 0 { return -1 } 140 if blocker_idx >= g.n_tasks { return -1 } 141 if task_idx == blocker_idx { return -1 } // self-cycle 142 let t: *Task = task_at(g, task_idx) 143 if t.blocked_by == (0 as *i64) { 144 t.blocked_by = sys_mmap(MAX_DEPS * 8 + 16) as *i64 145 t.n_blocked = 0 146 } 147 if t.n_blocked >= MAX_DEPS { return -2 } 148 t.blocked_by[t.n_blocked] = blocker_idx 149 t.n_blocked = t.n_blocked + 1 150 return 0 151} 152 153// Is this task ready to claim? A task is ready iff: 154// - state is OPEN 155// - every blocker is CLOSED 156func nxtask_is_ready(g: *TaskGraph, idx: i64) -> i64 { 157 if idx < 0 { return 0 } 158 if idx >= g.n_tasks { return 0 } 159 let t: *Task = task_at(g, idx) 160 if t.state != TASK_STATE_OPEN { return 0 } 161 var i: i64 = 0 162 while i < t.n_blocked { 163 let b_idx: i64 = t.blocked_by[i] 164 if b_idx >= 0 { 165 if b_idx < g.n_tasks { 166 let b: *Task = task_at(g, b_idx) 167 if b.state != TASK_STATE_CLOSED { return 0 } 168 } 169 } 170 i = i + 1 171 } 172 return 1 173} 174 175// === claim ========================================================== 176 177// Atomic claim: if task is open, mark it claimed by `agent`. 178// Returns 0 on success, -1 if already claimed or closed, -2 if 179// task index out of bounds. 180// 181// Atomicity note: v0.0.1 is single-process so the check-then-write 182// is trivially atomic from the caller's view. v0.1.0 adds proper 183// file-system-level locking via rename(). 184func nxtask_claim(g: *TaskGraph, idx: i64, 185 agent: *u8, agent_len: i64, 186 now_ns: i64) -> i64 { 187 if idx < 0 { return -2 } 188 if idx >= g.n_tasks { return -2 } 189 let t: *Task = task_at(g, idx) 190 if t.state != TASK_STATE_OPEN { return -1 } 191 t.state = TASK_STATE_CLAIMED 192 let cbuf: *u8 = sys_mmap(agent_len + 16) 193 var i: i64 = 0 194 while i < agent_len { 195 cbuf[i] = agent[i] 196 i = i + 1 197 } 198 cbuf[agent_len] = 0 199 t.claimed_by = cbuf 200 t.claimed_by_len = agent_len 201 t.updated_ns = now_ns 202 return 0 203} 204 205// === close ========================================================== 206 207// Close a task with a completion summary. Returns 0 on success, 208// -1 if task wasn't claimed or already closed. 209func nxtask_close(g: *TaskGraph, idx: i64, 210 summary: *u8, summary_len: i64, 211 now_ns: i64) -> i64 { 212 if idx < 0 { return -2 } 213 if idx >= g.n_tasks { return -2 } 214 let t: *Task = task_at(g, idx) 215 if t.state == TASK_STATE_CLOSED { return -1 } 216 t.state = TASK_STATE_CLOSED 217 let sbuf: *u8 = sys_mmap(summary_len + 16) 218 var i: i64 = 0 219 while i < summary_len { 220 sbuf[i] = summary[i] 221 i = i + 1 222 } 223 sbuf[summary_len] = 0 224 t.summary = sbuf 225 t.summary_len = summary_len 226 t.updated_ns = now_ns 227 return 0 228} 229 230// === query ========================================================== 231 232// Count tasks in a given state. 233func nxtask_count_state(g: *TaskGraph, state: i64) -> i64 { 234 var n: i64 = 0 235 var i: i64 = 0 236 while i < g.n_tasks { 237 let t: *Task = task_at(g, i) 238 if t.state == state { n = n + 1 } 239 i = i + 1 240 } 241 return n 242} 243 244// Find the first ready task (OPEN with all blockers CLOSED) for 245// an agent to claim. Now respects the dependency graph. 246func nxtask_next_ready(g: *TaskGraph) -> i64 { 247 var i: i64 = 0 248 while i < g.n_tasks { 249 if nxtask_is_ready(g, i) == 1 { return i } 250 i = i + 1 251 } 252 return -1 253} 254 255// === self-test ====================================================== 256 257func main() -> i64 { 258 let g: *TaskGraph = nxtask_graph_new(16) 259 260 let d1: *u8 = "Port regalloc to runtime/regalloc.nx" as *u8 261 let id1: i64 = nxtask_add(g, d1, 37, TASK_MAGIC_1000000000) 262 if id1 != 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 263 264 let d2: *u8 = "Add F-extension codegen" as *u8 265 let id2: i64 = nxtask_add(g, d2, 24, TASK_MAGIC_1000001000) 266 if id2 != 1 { return __syscall(93, 51, 0, 0, 0, 0, 0) } 267 268 // Counts. 269 if g.n_tasks != 2 { return __syscall(93, 52, 0, 0, 0, 0, 0) } 270 if nxtask_count_state(g, TASK_STATE_OPEN) != 2 { 271 return __syscall(93, 53, 0, 0, 0, 0, 0) 272 } 273 274 // Ready task: first one. 275 let next_id: i64 = nxtask_next_ready(g) 276 if next_id != 0 { return __syscall(93, 54, 0, 0, 0, 0, 0) } 277 278 // Claim + verify. 279 let agent: *u8 = "mormon-v0" as *u8 280 let rc: i64 = nxtask_claim(g, 0, agent, 9, TASK_MAGIC_1000002000) 281 if rc != 0 { return __syscall(93, 55, 0, 0, 0, 0, 0) } 282 let t0: *Task = task_at(g, 0) 283 if t0.state != TASK_STATE_CLAIMED { 284 return __syscall(93, 56, 0, 0, 0, 0, 0) 285 } 286 287 // Re-claiming fails. 288 let rc2: i64 = nxtask_claim(g, 0, agent, 9, TASK_MAGIC_1000003000) 289 if rc2 != -1 { return __syscall(93, 57, 0, 0, 0, 0, 0) } 290 291 // Next ready skips claimed task. 292 let next_id2: i64 = nxtask_next_ready(g) 293 if next_id2 != 1 { return __syscall(93, 58, 0, 0, 0, 0, 0) } 294 295 // Close with summary. 296 let smry: *u8 = "Done: rematerialisation ported to runtime" as *u8 297 let rc3: i64 = nxtask_close(g, 0, smry, 41, TASK_MAGIC_1000005000) 298 if rc3 != 0 { return __syscall(93, 59, 0, 0, 0, 0, 0) } 299 if nxtask_count_state(g, TASK_STATE_CLOSED) != 1 { 300 return __syscall(93, 60, 0, 0, 0, 0, 0) 301 } 302 303 // Dependency test: add a 3rd task that depends on task 1 304 // (which is still OPEN). Task 1 is ready; task 2 is blocked. 305 let d3: *u8 = "Wire F-extension codegen" as *u8 306 let id3: i64 = nxtask_add(g, d3, 24, TASK_MAGIC_1000006000) 307 if id3 != 2 { return __syscall(93, 61, 0, 0, 0, 0, 0) } 308 let drc: i64 = nxtask_add_dep(g, id3, 1) 309 if drc != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) } 310 311 // Task 1 is ready (no blockers). 312 if nxtask_is_ready(g, 1) != 1 { 313 return __syscall(93, 63, 0, 0, 0, 0, 0) 314 } 315 // Task 2 is blocked by task 1 (which isn't closed yet). 316 if nxtask_is_ready(g, 2) != 0 { 317 return __syscall(93, 64, 0, 0, 0, 0, 0) 318 } 319 // next_ready returns task 1 (the only ready one). 320 let nxt: i64 = nxtask_next_ready(g) 321 if nxt != 1 { 322 return __syscall(93, 65, 0, 0, 0, 0, 0) 323 } 324 325 // Close task 1. Now task 2 is ready. 326 let smry2: *u8 = "F-ext scaffolded" as *u8 327 nxtask_close(g, 1, smry2, 16, TASK_MAGIC_1000007000) 328 if nxtask_is_ready(g, 2) != 1 { 329 return __syscall(93, 66, 0, 0, 0, 0, 0) 330 } 331 332 return __syscall(93, 42, 0, 0, 0, 0, 0) 333}