code wiki / _hdl_build / nx_toolatency.nx
nx_toolatency.nx source
↩ module page · 559 lines · 29225 B
1// nx_toolatency.nx -- LATENCY CENSUS FOR THE TOOL SURFACE: the evidence behind async-lane routing.
2//
3// WHY THIS EXISTS (2026-08-07). The 503 class is a LATENCY problem. The edge closes its window at 15s
4// (NX_SD2_BACKEND_TIMEOUT_S) and the synchronous lane is budgeted at 14s (TEA_EXEC_TIMEOUT_SYNC_MS),
5// so a call that outruns the budget becomes a 503 WHOSE WORK MAY STILL HAVE LANDED -- the caller is
6// told it failed for a row that exists. async_only_tools.conf fixes that by forcing a tool onto the
7// detached job lane, but its membership was a HAND-WRITTEN LIST of tools somebody noticed failing.
8// You cannot route by a latency you never measured, and until today the daemon logged WHAT ran and
9// WHETHER it worked but never HOW LONG. ta_actlog now emits dur_ms (TOTAL HANDLER TIME, the quantity
10// the edge window actually applies -- exec time alone would understate it). This organ is the
11// consumer that turns those rows into a routing decision, so the lane stops being folklore.
12//
13// TAIL-ANCHORED BY CONSTRUCTION. actlog.jrnl is APPEND-ONLY, so a head-anchored capped read keeps the
14// OLDEST bytes and silently drops the NEWEST -- exactly backwards for a gauge whose job is to notice
15// what got slow LATELY. Measured cost of that mistake in this same estate: nx_wirecensus read this
16// very ledger head-anchored and was blind to 89 invocation records.
17//
18// REFUSES rather than reporting GREEN over data it never read: no-slow-tools and no-measurements are
19// different facts and must not share a representation.
20//
21// DURABLE + ROSTERABLE (2026-08-27, 503 lane): every run appends ONE row to
22// knowledge/status/toolatency.log (its own file, one writer -- the rm_trend_log law), so two runs are
23// a TREND and a fired beat is distinguishable from a dead one. The row carries `unrouted=` -- the
24// over-budget tools still on the sync lane -- so the count travels with its worklist. Refusals write
25// a NODATA row too: an abstention that leaves no artifact is indistinguishable from a beat that never
26// fired.
27import "nx_syscalls.nx"
28import "nx_toolatency_observation_lib.nx"
29
30const TL_TAIL_CAP: i64 = 4194304
31const TL_MAXTOOLS: i64 = 512
32const TL_NAMELEN: i64 = 48
33const TL_SYNC_BUDGET_MS: i64 = 14000
34const TL_WARN_PERMIL: i64 = 500
35const TL_CONFCAP: i64 = 65536
36const TL_SEEK_SET: i64 = 0
37const TL_SEEK_END: i64 = 2
38const TL_EXIT_SHOULD: i64 = 4
39const TL_EXIT_NODATA: i64 = 5
40const TL_EXIT_OVER: i64 = 6
41// A single call past the window is an OUTLIER; a tool that breaches repeatedly is BROKEN for that
42// lane. Rerouting changes the caller contract, so it must answer to a recurring breach and never to
43// one slow sample. Measured case that forced this: nx_mgmt showed max_ms=14002 -- TWO MILLISECONDS
44// past the budget -- across 69 calls averaging 1663 ms, i.e. one outlier, while nishi_compare_regen
45// breached on BOTH of its 2 calls at an average of 14854 ms. Max alone cannot tell those apart.
46const TL_MIN_BREACH: i64 = 2
47// The beat's durable artifact (ONE WRITER, ONE LOG, ONE ROW) + the bounded worklist buffer for the
48// unrouted= field. 4096 covers ~150 tool names against a measured worst of 21 offenders; a fuller
49// buffer ANNOUNCES with a +MORE sentinel rather than truncating silently.
50const TL_JRNL: *u8 = "knowledge/status/toolatency.log" as *u8
51// THE RANK TABLE (2026-09-15). The tools API orders tools/list by the presented cap, then by THESE counts
52// (most-called first), then newest-registered first -- see nx_tools_api.nx ta_emit_tools, and the measured
53// reason: a seat's MCP client walked 20 pages of 64 and stopped, so the ~700 newest of 1,977 registered
54// tools were invisible to every seat. The path is TA_RANK_DIR + the plane's basename + TA_RANK_EXT for the
55// production plane knowledge/toolreg-, spelled here ONCE so the writer and the reader name the same file;
56// nx_tools_api_gate proves the reader on a fixture plane and never touches this file.
57const TL_RANK_PATH: *u8 = "knowledge/status/toolrank-toolreg-.tsv" as *u8
58const TL_RANK_TMP: *u8 = "knowledge/status/toolrank-toolreg-.tsv.tmp" as *u8
59const TL_RANK_ROW_EXTRA: i64 = 48 // per row beyond the name: two decimals (<=20 digits each), two tabs, a newline, slack
60const TL_CSV_CAP: i64 = 4096
61// ACTION THRESHOLDS -- the pre-declared accept rule separating ROUTE (change the caller contract)
62// from TOLERATE (watch; the live sync-promote lane covers the tail). Without a rate/avg dimension
63// the census goes PERMANENTLY RED on high-volume fast tools (measured 2026-08-27: nx_fs, 19 breaches
64// in 12,141 calls = 1.6 permil -- rerouting it would tax twelve thousand fast reads with a poll to
65// save nineteen slow ones), and a permanently red detector is one everyone learns to ignore.
66// CONF IS THE SSOT (knowledge/toolatency.conf, rows act_permil= / act_avg_ms=, reasoning lives
67// there); these are the rule-17 bootstrap defaults, live only while the conf is absent.
68const TL_CONF: *u8 = "knowledge/toolatency.conf" as *u8
69const TL_ACT_PERMIL_DFLT: i64 = 40
70const TL_ACT_AVG_MS_DFLT: i64 = 2000
71// AUTO-ROUTE (2026-09-02). The accept rule below was applied BY HAND three times (08-27, 08-28, 08-29):
72// each time a seat copied this census into async_only_tools.conf while the OVER-BUDGET tools kept 503ing
73// for the day in between (measured 2026-09-02: nx_swarm_gpu, nx_frontier_put and nx_frontier_board sat
74// RED and unrouted across 20 consecutive hourly rows). A RULE APPLIED IDENTICALLY THREE TIMES BY A HUMAN
75// IS A RULE THAT BELONGS IN THE PATH. Conf-gated: knowledge/toolatency.conf auto_route=1 arms it; the
76// compiled default is OFF so the beat stays byte-identical until the operator turns the row on.
77const TL_AUTO_ROUTE_DFLT: i64 = 0
78const TL_ASYNC_CONF: *u8 = "async_only_tools.conf" as *u8
79const TL_CONF_MODE: i64 = 420 // 0644, the mode every other append-only conf/journal in the estate uses
80
81func tw(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
82func twe(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
83func twn(v: i64) -> i64 {
84 var m: i64 = v
85 if m < 0 { tw("-" as *u8); m = 0 - m }
86 let t: *u8 = sys_mmap(28)
87 var k: i64 = 0
88 if m == 0 { t[0] = 48 as u8; k = 1 }
89 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
90 let o: *u8 = sys_mmap(28)
91 var i: i64 = 0
92 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
93 sys_write(1, o, k)
94 sys_munmap(t, 28)
95 sys_munmap(o, 28)
96 return 0
97}
98
99// small bounded buffer cats, local ON PURPOSE: importing a string lib into a leaf census organ would
100// drag a module in for two 15-line helpers. Same shapes as ta_cat/ta_catn.
101func tl_cat(b: *u8, o: i64, s: *u8) -> i64 {
102 var i: i64 = 0
103 var p: i64 = o
104 while s[i] != (0 as u8) { b[p] = s[i]; p = p + 1; i = i + 1 }
105 return p
106}
107func tl_catn(b: *u8, o: i64, v: i64) -> i64 {
108 var m: i64 = v
109 var p: i64 = o
110 if m < 0 { b[p] = 45 as u8; p = p + 1; m = 0 - m }
111 let t: *u8 = sys_mmap(28)
112 var k: i64 = 0
113 if m == 0 { t[0] = 48 as u8; k = 1 }
114 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
115 var i: i64 = 0
116 while i < k { b[p + i] = t[k - 1 - i]; i = i + 1 }
117 sys_munmap(t, 28)
118 return p + k
119}
120// ONE WRITER, ONE LOG, ONE ROW (the rm_trend_log law, nx_resmon): the durable per-run artifact, so a
121// census whose verdict was never persisted stops being a private opinion and two rows become a trend.
122// CLOSED VOCABULARY (GREEN|AMBER|RED|NODATA) so no rollup ever meets a new dialect. unrouted= names
123// the over-budget-and-still-sync tools -- the row carries its own worklist, because a count without a
124// worklist is not actionable. An unwritable journal ANNOUNCES on stderr rather than vanishing.
125// auto_routed= (2026-09-02) rides BEFORE verdict= so verdict stays the LAST token (gv_last_line anchors by
126// position) and every key= scanner keeps working; it is what lets a reader of this journal tell "still
127// unrouted" from "routed on this very row", which the unrouted= list alone cannot.
128func tl_jrnl(rows: i64, measured: i64, ntools: i64, over: i64, atrisk: i64, routed: i64, unrouted: *u8, verdict: *u8) -> i64 {
129 let fd: i64 = sys_openat_append(TL_JRNL, MODE_0644)
130 if fd < 0 {
131 twe("NX-TOOLATENCY jrnl-unwritable: " as *u8)
132 twe(TL_JRNL)
133 twe(" -- row lost, said out loud\n" as *u8)
134 return 0 - 1
135 }
136 let b: *u8 = sys_mmap(TL_CSV_CAP + 256)
137 var o: i64 = tl_cat(b, 0, "ts=" as *u8)
138 o = tl_catn(b, o, sys_now_realtime_sec())
139 o = tl_cat(b, o, " rows=" as *u8); o = tl_catn(b, o, rows)
140 o = tl_cat(b, o, " measured=" as *u8); o = tl_catn(b, o, measured)
141 o = tl_cat(b, o, " tools=" as *u8); o = tl_catn(b, o, ntools)
142 o = tl_cat(b, o, " over_budget=" as *u8); o = tl_catn(b, o, over)
143 o = tl_cat(b, o, " at_risk=" as *u8); o = tl_catn(b, o, atrisk)
144 o = tl_cat(b, o, " unrouted=" as *u8)
145 o = tl_cat(b, o, unrouted)
146 o = tl_cat(b, o, " auto_routed=" as *u8); o = tl_catn(b, o, routed)
147 o = tl_cat(b, o, " verdict=" as *u8)
148 o = tl_cat(b, o, verdict)
149 b[o] = 10 as u8
150 sys_write(fd, b, o + 1)
151 sys_close(fd)
152 return 0
153}
154
155// TAIL-ANCHORED read: when the ledger exceeds the window, seek so the window ENDS at EOF.
156func tl_read_tail(path: *u8, buf: *u8, cap: i64) -> i64 {
157 let fd: i64 = sys_openat_rd(path)
158 if fd < 0 { return 0 - 1 }
159 let sz: i64 = sys_lseek(fd, 0, TL_SEEK_END)
160 if sz > cap { sys_lseek(fd, sz - cap, TL_SEEK_SET) } else { sys_lseek(fd, 0, TL_SEEK_SET) }
161 let n: i64 = sys_read(fd, buf, cap)
162 sys_close(fd)
163 return n
164}
165
166func tl_eol(buf: *u8, n: i64, i: i64) -> i64 {
167 var e: i64 = i
168 var go: i64 = 1
169 while go == 1 {
170 if e >= n { go = 0 } else { if buf[e] == (10 as u8) { go = 0 } else { e = e + 1 } }
171 }
172 return e
173}
174
175// value following key= within [s,e). -1 = key absent, which means UNMEASURED and never 0.
176func tl_kv(buf: *u8, s: i64, e: i64, key: *u8) -> i64 {
177 var kl: i64 = 0
178 while key[kl] != (0 as u8) { kl = kl + 1 }
179 var i: i64 = s
180 while i + kl <= e {
181 var k: i64 = 0
182 var ok: i64 = 1
183 while k < kl { if buf[i + k] != key[k] { ok = 0; k = kl } else { k = k + 1 } }
184 if ok == 1 {
185 var j: i64 = i + kl
186 var v: i64 = 0
187 var any: i64 = 0
188 var go: i64 = 1
189 while go == 1 {
190 go = 0
191 if j < e {
192 let c: i64 = buf[j] as i64
193 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; j = j + 1; go = 1 } }
194 }
195 }
196 if any == 0 { return 0 - 1 }
197 return v
198 }
199 i = i + 1
200 }
201 return 0 - 1
202}
203
204// exact-line membership in async_only_tools.conf. CR-tolerant. MIRRORS THE ENFORCER: the daemon's
205// ta_name_in_lines (nx_tools_api.nx, seq1293) matches the WHOLE line against the tool name, so a
206// per-verb "NAME VERB" row can NEVER match there and is inert. This reporter used to also accept the
207// NAME-space prefix -- two readers of one conf with two grammars -- and so displayed lane=async for
208// tools the daemon demonstrably ran sync (measured 2026-08-27: the inert "nx_debt add" rows).
209// The reporter must speak the enforcer's grammar or it lies about lanes.
210func tl_on_lane(conf: *u8, n: i64, nm: *u8, nl: i64) -> i64 {
211 var i: i64 = 0
212 while i < n {
213 let e: i64 = tl_eol(conf, n, i)
214 var len: i64 = e - i
215 if len > 0 { if conf[i + len - 1] == (13 as u8) { len = len - 1 } }
216 if len >= nl {
217 var k: i64 = 0
218 var same: i64 = 1
219 while k < nl { if conf[i + k] != nm[k] { same = 0; k = nl } else { k = k + 1 } }
220 if same == 1 {
221 if len == nl { return 1 }
222 }
223 }
224 i = e + 1
225 }
226 return 0
227}
228
229// ---- AUTO-ROUTE writers (2026-09-02; see TL_AUTO_ROUTE_DFLT) --------------------------------------------
230// One append-row per routed tool: a comment carrying the measurement that admitted it (the same shape the
231// hand-adopted 08-28/08-29 rows carry, so the conf stays adjudicable without re-running the census), then
232// the bare name on its OWN line -- the enforcer's grammar is exact-whole-line (ta_name_in_lines), so the
233// name must stand alone. Returns the new offset; returns `o` unchanged (nothing appended) when the buffer
234// cannot take a whole row, so a half-written row can never be emitted.
235func tl_auto_route_row(b: *u8, o: i64, nm: *u8, nl: i64, breaches: i64, calls: i64, avg: i64) -> i64 {
236 var p: i64 = o
237 if p + nl + nl + 160 >= TL_CONFCAP { return o }
238 p = tl_cat(b, p, "# AUTO-ROUTED " as *u8)
239 p = tl_catn(b, p, sys_now_realtime_sec())
240 p = tl_cat(b, p, " by nx_toolatency under the standing accept rule (breaches>=2 AND (permil>=act_permil OR avg>=act_avg_ms)): " as *u8)
241 var k: i64 = 0
242 while k < nl { b[p] = nm[k]; p = p + 1; k = k + 1 }
243 p = tl_cat(b, p, "(b=" as *u8)
244 p = tl_catn(b, p, breaches)
245 b[p] = 47 as u8
246 p = p + 1
247 p = tl_catn(b, p, calls)
248 p = tl_cat(b, p, ",avg=" as *u8)
249 p = tl_catn(b, p, avg)
250 p = tl_cat(b, p, ")\n" as *u8)
251 k = 0
252 while k < nl { b[p] = nm[k]; p = p + 1; k = k + 1 }
253 b[p] = 10 as u8
254 p = p + 1
255 return p
256}
257// ONE open, ONE write, ONE close, after the census: the conf is hot-read per call by the daemon, so the
258// very next call of each routed tool is already on the job lane. An unwritable conf is ANNOUNCED with its
259// rc -- a silent no-op here would read on the next census as "still unrouted" with no cause.
260func tl_auto_route_apply(b: *u8, n: i64, routed: i64) -> i64 {
261 let fd: i64 = sys_openat_append(TL_ASYNC_CONF, TL_CONF_MODE)
262 if fd < 0 {
263 tw("NX-TOOLATENCY auto_route=1 but " as *u8); tw(TL_ASYNC_CONF); tw(" is UNWRITABLE rc=" as *u8); twn(fd); tw(" -- NOTHING routed; the tools above stay on the sync lane\n" as *u8)
264 return 0 - 1
265 }
266 let w: i64 = sys_write(fd, b, n)
267 sys_close(fd)
268 tw("NX-TOOLATENCY AUTO-ROUTED " as *u8); twn(routed); tw(" tool(s) -> " as *u8); tw(TL_ASYNC_CONF); tw(" (+" as *u8); twn(w); tw(" B, one append; the daemon hot-reads it, so the NEXT call of each is already on the job lane)\n" as *u8)
269 return w
270}
271
272// Write the rank table ATOMICALLY (temp + rename): every tool this census saw, most-called first, ties by
273// first-seen order. The tools API reads it once per tools/list request, and a torn file would reorder a
274// seat's surface mid-page, so the rename is the publish. Returns rows written; -1 when the write was refused
275// (the previous table then stands untouched -- a stale rank beats a torn one).
276func tl_write_rank(names: *u8, cnt: *i64, ntools: i64, asof: i64) -> i64 {
277 let rcap: i64 = TL_MAXTOOLS * (TL_NAMELEN + TL_RANK_ROW_EXTRA)
278 let rb: *u8 = sys_mmap(rcap + 16)
279 let done: *i64 = sys_mmap(8 * TL_MAXTOOLS + 16) as *i64
280 var o: i64 = tl_cat(rb, 0, "# name<TAB>calls<TAB>asof -- written by nx_toolatency from actlog.jrnl; read by nx_tools_api ta_emit_tools (tier 2 of the list order)\n" as *u8)
281 var rows: i64 = 0
282 var more: i64 = 1
283 while more == 1 {
284 var best: i64 = 0 - 1
285 var q: i64 = 0
286 while q < ntools { if done[q] == 0 { if best < 0 { best = q } else { if cnt[q] > cnt[best] { best = q } } } q = q + 1 }
287 if best < 0 { more = 0 } else {
288 done[best] = 1
289 let nmp: *u8 = ((names as i64) + best * TL_NAMELEN) as *u8
290 if o + TL_NAMELEN + TL_RANK_ROW_EXTRA >= rcap { more = 0 } else {
291 o = tl_cat(rb, o, nmp)
292 rb[o] = 9 as u8; o = o + 1
293 o = tl_catn(rb, o, cnt[best])
294 rb[o] = 9 as u8; o = o + 1
295 o = tl_catn(rb, o, asof)
296 rb[o] = 10 as u8; o = o + 1
297 rows = rows + 1
298 }
299 }
300 }
301 let fd: i64 = sys_openat_wr(TL_RANK_TMP, TL_CONF_MODE)
302 if fd < 0 { return 0 - 1 }
303 sys_write(fd, rb, o)
304 sys_close(fd)
305 if sys_renameat(TL_RANK_TMP, TL_RANK_PATH) != 0 { return 0 - 1 }
306 return rows
307}
308
309func main(argc: i64, argv: *i64) -> i64 {
310 if argc > 1 {
311 let command: *u8=argv[1] as *u8
312 if to_eq(command,0,to_len(command),"observations" as *u8) == 1 {
313 if sys_ignore_sigpipe() != 0 { return TO_STATUS_OUTPUT }
314 return to_observations(argc,argv)
315 }
316 }
317 let buf: *u8 = sys_mmap(TL_TAIL_CAP + 16)
318 let n: i64 = tl_read_tail("knowledge/status/actlog.jrnl" as *u8, buf, TL_TAIL_CAP)
319 if n <= 0 {
320 twe("NX-TOOLATENCY REFUSED: knowledge/status/actlog.jrnl unreadable or empty -- refusing to report GREEN over a ledger it never read\n" as *u8)
321 tl_jrnl(0, 0, 0, 0, 0, 0, "-" as *u8, "NODATA" as *u8)
322 sys_exit(TL_EXIT_NODATA)
323 return TL_EXIT_NODATA
324 }
325 let names: *u8 = sys_mmap(TL_MAXTOOLS * TL_NAMELEN + 16)
326 let cnt: *i64 = sys_mmap(8 * TL_MAXTOOLS + 16) as *i64
327 let mx: *i64 = sys_mmap(8 * TL_MAXTOOLS + 16) as *i64
328 let sm: *i64 = sys_mmap(8 * TL_MAXTOOLS + 16) as *i64
329 let br: *i64 = sys_mmap(8 * TL_MAXTOOLS + 16) as *i64
330 var ntools: i64 = 0
331 var rows: i64 = 0
332 var measured: i64 = 0
333
334 var i: i64 = 0
335 if n >= TL_TAIL_CAP { i = tl_eol(buf, n, 0) + 1 }
336
337 while i < n {
338 let e: i64 = tl_eol(buf, n, i)
339 var t2: i64 = 0 - 1
340 var t3: i64 = 0 - 1
341 var seen: i64 = 0
342 var p: i64 = i
343 while p < e {
344 if buf[p] == (9 as u8) {
345 seen = seen + 1
346 if seen == 2 { t2 = p }
347 if seen == 3 { t3 = p }
348 }
349 p = p + 1
350 }
351 if t2 >= 0 { if t3 > t2 {
352 rows = rows + 1
353 let d: i64 = tl_kv(buf, i, e, "dur_ms=" as *u8)
354 if d >= 0 {
355 measured = measured + 1
356 var nl2: i64 = t3 - t2 - 1
357 if nl2 > TL_NAMELEN - 1 { nl2 = TL_NAMELEN - 1 }
358 var idx: i64 = 0 - 1
359 var q: i64 = 0
360 while q < ntools {
361 var k: i64 = 0
362 var same: i64 = 1
363 while k < nl2 { if names[q * TL_NAMELEN + k] != buf[t2 + 1 + k] { same = 0; k = nl2 } else { k = k + 1 } }
364 if same == 1 { if names[q * TL_NAMELEN + nl2] == (0 as u8) { idx = q; q = ntools } else { q = q + 1 } } else { q = q + 1 }
365 }
366 if idx < 0 {
367 if ntools < TL_MAXTOOLS {
368 idx = ntools
369 var k2: i64 = 0
370 while k2 < nl2 { names[idx * TL_NAMELEN + k2] = buf[t2 + 1 + k2]; k2 = k2 + 1 }
371 names[idx * TL_NAMELEN + nl2] = 0 as u8
372 cnt[idx] = 0
373 mx[idx] = 0
374 sm[idx] = 0
375 br[idx] = 0
376 ntools = ntools + 1
377 }
378 }
379 if idx >= 0 {
380 cnt[idx] = cnt[idx] + 1
381 sm[idx] = sm[idx] + d
382 if d > mx[idx] { mx[idx] = d }
383 if d >= TL_SYNC_BUDGET_MS { br[idx] = br[idx] + 1 }
384 }
385 }
386 } }
387 i = e + 1
388 }
389
390 let cf: *u8 = sys_mmap(TL_CONFCAP + 16)
391 var cn: i64 = 0
392 let cfd: i64 = sys_openat_rd("async_only_tools.conf" as *u8)
393 if cfd >= 0 { cn = sys_read(cfd, cf, TL_CONFCAP); sys_close(cfd) }
394
395 // action thresholds: conf first (SSOT), bootstrap defaults otherwise. tl_kv returns -1 for an
396 // absent key, and -1 must never become a threshold -- absent means default, decided per key.
397 var act_permil: i64 = TL_ACT_PERMIL_DFLT
398 var act_avg: i64 = TL_ACT_AVG_MS_DFLT
399 let tcf: *u8 = sys_mmap(TL_CONFCAP + 16)
400 var tcn: i64 = 0
401 let tfd: i64 = sys_openat_rd(TL_CONF)
402 if tfd >= 0 { tcn = sys_read(tfd, tcf, TL_CONFCAP); sys_close(tfd) }
403 if tcn > 0 {
404 let cpm: i64 = tl_kv(tcf, 0, tcn, "act_permil=" as *u8)
405 if cpm >= 0 { act_permil = cpm }
406 let cav: i64 = tl_kv(tcf, 0, tcn, "act_avg_ms=" as *u8)
407 if cav >= 0 { act_avg = cav }
408 }
409 // AUTO-ROUTE arm (see TL_AUTO_ROUTE_DFLT): conf row wins, compiled default OFF. tl_kv returns -1 for an
410 // absent key and -1 must never become a mode -- absent means default, decided here.
411 var auto_route: i64 = TL_AUTO_ROUTE_DFLT
412 if tcn > 0 { let car: i64 = tl_kv(tcf, 0, tcn, "auto_route=" as *u8); if car >= 0 { auto_route = car } }
413 let apb: *u8 = sys_mmap(TL_CONFCAP + 16)
414 var apo: i64 = 0
415 var routed: i64 = 0
416
417 let warn_ms: i64 = TL_SYNC_BUDGET_MS * TL_WARN_PERMIL / 1000
418 tw("=== nx_toolatency -- measured handler latency vs the sync budget ===\n" as *u8)
419 tw("rows=" as *u8); twn(rows)
420 tw(" measured=" as *u8); twn(measured)
421 tw(" tools=" as *u8); twn(ntools)
422 tw(" sync_budget_ms=" as *u8); twn(TL_SYNC_BUDGET_MS)
423 tw(" warn_ms=" as *u8); twn(warn_ms)
424 tw(" lane_conf_bytes=" as *u8); twn(cn)
425 tw(" act_permil=" as *u8); twn(act_permil)
426 tw(" act_avg_ms=" as *u8); twn(act_avg)
427 if tcn > 0 { tw(" act_src=conf" as *u8) } else { tw(" act_src=default" as *u8) }
428 tw("\n" as *u8)
429
430 if measured == 0 {
431 twe("NX-TOOLATENCY REFUSED: not one row carries dur_ms -- this ledger predates the telemetry, so GREEN here would mean UNMEASURED, not FAST\n" as *u8)
432 tl_jrnl(rows, 0, ntools, 0, 0, 0, "-" as *u8, "NODATA" as *u8)
433 sys_exit(TL_EXIT_NODATA)
434 return TL_EXIT_NODATA
435 }
436
437 // THREE TIERS, arrived at by being wrong twice and letting the data correct the ruler.
438 // v1 flagged everything past HALF the budget as SHOULD-ASYNC -- that conflates an ALERT
439 // threshold with an ACTION threshold, and rerouting CHANGES THE CALLER CONTRACT (the caller
440 // gets a job id instead of a result), so it must never fire on a tool that still works.
441 // v2 acted on MAX past the full window -- but max cannot tell a systematic breach from one bad
442 // sample. It immediately flagged nx_mgmt, the tool that drives every build and deploy, on a
443 // max of 14002 ms: TWO MILLISECONDS over, across 69 calls averaging 1663. Rerouting the whole
444 // management API on that would have been an overreaction caused by the ruler, not the tool.
445 // v3 counts BREACHES. nishi_compare_regen breached on both of its 2 calls (avg 14854) -- that is
446 // a tool that does not fit the lane. nx_mgmt breached once in 69 -- that is an outlier.
447 // OVER-BUDGET = repeated breaches, act. OUTLIER = crossed once, watch. AT-RISK = past half, watch.
448 // the journal row's worklist: over-budget tools still on the sync lane, comma-joined, bounded.
449 let unr: *u8 = sys_mmap(TL_CSV_CAP + 16)
450 var uo: i64 = 0
451 var utrunc: i64 = 0
452 unr[0] = 0 as u8
453 var over: i64 = 0
454 var atrisk: i64 = 0
455 var should: i64 = 0
456 var q2: i64 = 0
457 while q2 < ntools {
458 let nmp: *u8 = ((names as i64) + q2 * TL_NAMELEN) as *u8
459 var nl3: i64 = 0
460 while nmp[nl3] != (0 as u8) { nl3 = nl3 + 1 }
461 let onlane: i64 = tl_on_lane(cf, cn, nmp, nl3)
462 tw(" " as *u8); tw(nmp)
463 tw(" calls=" as *u8); twn(cnt[q2])
464 tw(" max_ms=" as *u8); twn(mx[q2])
465 tw(" avg_ms=" as *u8); twn(sm[q2] / cnt[q2])
466 if onlane == 1 { tw(" lane=async" as *u8) } else { tw(" lane=sync" as *u8) }
467 tw(" breaches=" as *u8); twn(br[q2])
468 if onlane == 0 {
469 // ROUTE vs TOLERATE: recurring breaches alone are not the action bar -- rerouting taxes
470 // EVERY call of a tool with a poll to save its slow tail, so the rule demands the tail be
471 // a real fraction of calls (act_permil) or the tool be slow even on average (act_avg_ms).
472 // Same rule, same values, that admitted the 2026-08-27 async_only rows: ruler and decision
473 // share arithmetic or they drift.
474 var act: i64 = 0
475 if br[q2] >= TL_MIN_BREACH {
476 if (br[q2] * 1000) / cnt[q2] >= act_permil { act = 1 }
477 if sm[q2] / cnt[q2] >= act_avg { act = 1 }
478 }
479 if act == 1 {
480 over = over + 1
481 should = should + 1
482 // THE REASON TRAVELS WITH THE COUNT. A worklist of bare names is one step short: the
483 // reader still has to re-run the whole census to learn WHY each name is on it, and the
484 // numbers are already in hand right here. MEASURED CASE THAT FORCED THIS (2026-08-27):
485 // the beat wrote `unrouted=nx_page_verify` and a hand-run minutes later showed that tool
486 // TOLERATED at 3/88 = 34 permil, just under the 40 bar -- the tail window slides, so a
487 // borderline tool oscillates across the bar and a bare name cannot say which side it was
488 // on when the row was written. Emitting name(b=<breaches>/<calls>,avg=<ms>) makes the row
489 // adjudicable on its own, with no re-run and no access to the census that produced it.
490 // The 24-byte headroom covers the widest reason this can emit (two 10-digit counts plus
491 // the punctuation); too little room marks utrunc rather than emitting a half-written one.
492 if uo + nl3 + 24 < TL_CSV_CAP {
493 if uo > 0 { unr[uo] = 44 as u8; uo = uo + 1 }
494 var uk: i64 = 0
495 while uk < nl3 { unr[uo] = nmp[uk]; uo = uo + 1; uk = uk + 1 }
496 uo = tl_cat(unr, uo, "(b=" as *u8)
497 uo = tl_catn(unr, uo, br[q2])
498 unr[uo] = 47 as u8
499 uo = uo + 1
500 uo = tl_catn(unr, uo, cnt[q2])
501 uo = tl_cat(unr, uo, ",avg=" as *u8)
502 uo = tl_catn(unr, uo, sm[q2] / cnt[q2])
503 unr[uo] = 41 as u8
504 uo = uo + 1
505 unr[uo] = 0 as u8
506 } else { utrunc = 1 }
507 // AUTO-ROUTE: accumulate the row now, write ONCE after the loop (one open, one write, one close).
508 if auto_route == 1 {
509 let apo2: i64 = tl_auto_route_row(apb, apo, nmp, nl3, br[q2], cnt[q2], sm[q2] / cnt[q2])
510 if apo2 > apo { apo = apo2; routed = routed + 1 }
511 }
512 tw(" *** OVER-BUDGET: breaches the sync window REPEATEDLY -- these calls 503 today, move to async_only_tools.conf" as *u8)
513 if auto_route == 1 { tw(" (AUTO-ROUTING NOW -- see the AUTO-ROUTED line below)" as *u8) }
514 tw(" ***" as *u8)
515 } else { if br[q2] >= TL_MIN_BREACH {
516 atrisk = atrisk + 1
517 tw(" *** TOLERATED: repeated breaches below the action bar (rate under act_permil, avg under act_avg_ms) -- high-volume fast tool, the sync-promote lane covers this tail; deliberately NOT rerouted ***" as *u8)
518 } else { if br[q2] > 0 {
519 atrisk = atrisk + 1
520 tw(" *** OUTLIER: crossed the window once -- watch it, do not change the caller contract on one sample ***" as *u8)
521 } else {
522 if mx[q2] >= warn_ms {
523 atrisk = atrisk + 1
524 tw(" *** AT-RISK: past half the sync budget -- watch, do not reroute yet (rerouting changes the caller contract) ***" as *u8)
525 }
526 } } }
527 }
528 tw("\n" as *u8)
529 q2 = q2 + 1
530 }
531
532 // RANK TABLE for the tools API list order (2026-09-15): published on every census run, whatever the verdict.
533 let rrows: i64 = tl_write_rank(names, cnt, ntools, sys_now_realtime_sec())
534 tw("NX-TOOLATENCY rank_rows=" as *u8); twn(rrows); tw(" rank_path=" as *u8); tw(TL_RANK_PATH); tw("\n" as *u8)
535 // AUTO-ROUTE: the accumulated rows land in ONE append, after the census and before the verdict.
536 if routed > 0 { tl_auto_route_apply(apb, apo, routed) }
537 tw("NX-TOOLATENCY tools=" as *u8); twn(ntools)
538 tw(" over_budget=" as *u8); twn(over)
539 tw(" at_risk=" as *u8); twn(atrisk)
540 tw(" auto_route=" as *u8); twn(auto_route)
541 tw(" auto_routed=" as *u8); twn(routed)
542 if utrunc == 1 { uo = tl_cat(unr, uo, ",+MORE" as *u8); unr[uo] = 0 as u8 }
543 if uo == 0 { uo = tl_cat(unr, 0, "-" as *u8); unr[uo] = 0 as u8 }
544 if over > 0 {
545 tw(" verdict=RED (the OVER-BUDGET tools are 503ing now; a 503 there loses a response for work that may have landed)\n" as *u8)
546 tl_jrnl(rows, measured, ntools, over, atrisk, routed, unr, "RED" as *u8)
547 sys_exit(TL_EXIT_OVER)
548 return TL_EXIT_OVER
549 }
550 if atrisk > 0 {
551 tw(" verdict=AMBER (nothing is failing yet; the AT-RISK tools are past half the window and worth watching)\n" as *u8)
552 tl_jrnl(rows, measured, ntools, over, atrisk, routed, unr, "AMBER" as *u8)
553 sys_exit(TL_EXIT_SHOULD)
554 return TL_EXIT_SHOULD
555 }
556 tw(" verdict=GREEN (every measured tool sits inside half the sync budget, or is already on the job lane)\n" as *u8)
557 tl_jrnl(rows, measured, ntools, over, atrisk, routed, unr, "GREEN" as *u8)
558 return 0
559}