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}