code wiki / (root) / nxtask.nx

nxtask.nx source

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