close() their maps — one why drive eats ~1000 fds, blinding the wt oracleRULING (gritzko, chat 2026-07-20): abc.close()/the buffer close now REALLY closes the file and unmaps (the ABC-020 release binding landed in jab, io.cpp:578 io._munmap = munmap + close(fd) + fd=-1) — so the remaining defect is on the JS side: *be/ JS maps and never closes*. Double-check the jab-side semantics first, then make every JS reader release deterministically. Discovered as the root cause of BRO-040 (pager context rot); recurrence of the same disease as JSQUE-021DONE (verb-load path) in the ulog/store read path.
shared/ulog.js:42-48 each() maps (abc.mmap("ULOG", path, "r")), iterates, and RETURNS — no close on ANY path, success or throw; _withRo (:265) already has the map-run-close idiom the readers should use.why <file> drive consumes cat/status drives leak ulimit -n 64, jab status completes, jab why CMakeLists.txt dies Error: No file descriptors available.abc.mmap throw, ulog.each swallows it (catch (e) { return; } → zero rows), anchors() reads that as "no worktree", the pager's file→dir oracle blinds, and a FILE persists as a dir context segment — the BRO-040 rot, with the error kind varying by which layer notices first../jab, that the close/unmap binding does what the ruling says: munmap + close(2) + fd=-1, idempotent, and safe against the GC finalizer firing later on the same record. Report the finding before relying on it.why/status drives is flat after the fix; the BRO-040 4-step click dance stops corrupting the context.jab exposes; if the binding is wrong, report, do not patch around it.ulog.each must tell an absent log from a resource error; coordinate, do not duplicate.why); ulimit -n 64 differentialabc.close does NOT release a map whose bytes were already MATERIALIZED — and every real reader materializes. abc.close (cont.cpp:422) is transfer-first (b.transfer() → husk → msync → trim → io._munmap(husk)); after any byte-pointer materialization (the native cursor next(), the row getters abc._ulog_time/_verb/_uri — all GetTypedArrayBytesPtr), ArrayBuffer.transfer() COPIES instead of moving, so the husk carries no mapping, io._munmap(husk) finds no live FILE map at that base, returns NONE silently, and the fd survives to GC.fdprobe.js, N cycles of map+close on a real .be ULOG): under ulimit -n 64, the NO-READ variant runs 400 cycles flat; the variant that adds ONE log.next() dies No file descriptors available after *57 cycles*. Unbounded, both finish 1500 — so release is DEFERRED TO GC, not absent; a fast drive (1005 maps) simply outruns the collector. jab's own test/mmapfd.js passes because it never reads through the container.transfer(). This is ABC-020 design space, whose rework rulings gritzko DISAVOWED — needs his verdict, not a worker's.why drives), so no store/why/blame sweep is needed.each() keeps catch (e) { return; }; keep (b) open.jab put, NO commit): shared/ulog.js (each/withRO/book → map-use-release-in-finally; _withRO did NOT already close, contra this ticket's premise), new test/js120/fdplateau.js (acceptance: fd count plateaus over N drives, exhausted process = FAIL) and test/js120/fdtrace.js (attribution probe: 1005 ULOG maps + 83 keeper packs + 64 main-tree .be per ONE why drive, abc.close calls ZERO).-LE big in-wt: 305/306, sole red work/getctx (pre-existing — fails identically with pristine ulog.js swapped back); bro/color + bro/view PASS. No existing test edited.JS-120: ulog readers release their maps at the frame boundary (map-use-close)