NNNNN.keeper log at 2 GiB — past that the log is unmappable, and the cap is unenforceable without also capping the fetchPackLog says "a new log starts only past a size threshold" but never bounds that threshold, so a single landed pack can make one log arbitrarily large. GET-044 T4 proved it: a linux.git clone streamed 6.45 GB and atomically landed a durable *6.0 GB* 0000000001.keeper — bytes on disk that no reader can open. RULING 2026-07-27 (gritzko): *cap at 2 GB, not at the engine's ceiling. The engine numbers, all measured this session on sparse files: JSC allocates a typed array up to 4 GiB inclusive, but only 2^32-1 of it is addressable (a[2^32-1] reads undefined, JS's canonical not-an-index sentinel), and the JAB-007 Buf | 0 cursor wraps NEGATIVE at 2^31 exactly, so io.mmap yields an empty or garbled view from there up. 2^31-1 is the only bound that survives the whole path, and ingest.js already refuses at exactly that (MMAP_CAP). jab/io.cpp JABC_MAP_MAX now enforces it in the mapper (this session). The sting in the tail: PackLog §Delta-dependency pins OFS_DELTA bases inside one pack*, so a received pack cannot be split across logs — meaning a 2 GB log cap is only achievable if no single fetched pack exceeds 2 GB. Scope: keeper log rotation + the fetch that feeds it. See PackLog, Keeper; method Issues.
wiki/PackLog.mkd §Many packs per log: "a new log starts only past a size threshold" — threshold unspecified, no upper bound, nothing refuses an oversized append.wiki/PackLog.mkd §Delta-dependency: "OFS_DELTA is pack-local: the base sits earlier in the same pack" — splitting one pack across two logs breaks every back-reference that crosses the cut.be/shared/ingest.js ships MMAP_CAP = 2147483647 and refuses cleanly (… landed OK but exceeds the jab 2^31-1 mmap cap) — a guard on the reader, not on what gets written.io.mmap (Buf-wrapped) behaviour: 2^31-1 → correct; 2^31 → size -2147483648, data() empty; 3 GB → size -1073741824, garbled 2 GB view; 4 GiB → size 0. Silent every time.io._mmap: exact lengths through 2^32 inclusive; 2^32+1 → was SIGABRT (exit 134), now a catchable throw.linux.git benchmark this session (2026-07-27) — git clone 1849 s / 6.3 GB .git; jab get died after pulling 6024 MB, so the 6.0 GB log from GET-044 T4 is the only landed instance.No keeper log ever exceeds 2 GB (2^31-1 bytes), and the writer refuses to create one instead of landing unreadable bytes.
KEEP_LOG_MAX (2^31-1) enforced at append time: an append that would cross it opens the next NNNNN.keeper instead.JavaScriptCore/ C API headers the binding is written against, so switching is a large port for zero gain.RULING 2026-07-27 (gritzko): log splitting is fine on the beagle side — the indexes already support multiple files. That makes the incoming pack, not the log, the binding constraint: a received pack cannot be split at an arbitrary byte because PackLog §Delta-dependency pins OFS_DELTA bases inside one pack. Every crossing delta must be re-anchored by hash. So the question became how much does REF_DELTA inflate the pack, and it was measured rather than argued.
*Measurements 2026-07-27* — git verify-pack -v over two real packs, base-reference bytes only (the zlib payload is byte-identical either way, so added = 20 - ofslen(distance) per converted edge; ofslen thresholds 128/16512/2113664/270549120 match git's offset encoding exactly). Verified every delta in the git.git pack is currently type 6 OFS_DELTA (315878/315878, type nibble read straight from the pack), so the baseline is real and nothing was already sha-coded.
git.git 300.9 MB pack 417,182 objs 315,878 deltas (75.7%) 2.76 B/delta option 1 all OFS 870,855 B baseline option 3 all REF +5,446,705 B +1.73% of pack option 2 crossings REF @2GiB cap 0 B 0.00% (fits one log)
@256MB +382,209 B (7.6% of deltas) +0.12%
linux.git 6.33 GB pack 11,690,992 objs 9,492,857 deltas (81.2%) 2.96 B/delta option 1 all OFS 28,067,830 B baseline option 3 all REF +161,789,310 B +2.56% of pack option 2 crossings REF @2GiB cap +24,089,888 B +0.38% (3 segs, 16.91% of deltas, 15.00 B/edge)
@1GiB +27,389,232 B +0.43% (6 segs, 19.22%)
@512MB +29,961,837 B +0.47% (12 segs, 20.99%) @256MB +32,195,316 B +0.51% (24 segs, 22.49%)
Three findings that decide it:
Bytes were never the real argument against all-REF. The other two costs are:
cur -= ofs_delta — one subtraction inside the already-mapped log. Under REF each is a hashlet range query across every LSM run in the shard, newest-wins (Indices §Range queries), i.e. a random probe into a second file then a jump back. All-REF turns 46M subtractions into 46M lookups, permanently, in every repo. Option 2 leaves DECISION: *option 2* — OFS within a pack/segment, REF only across boundaries. This is PackLog §Delta-dependency as already written, not a new design. The crossing set falls out of the scan pass ingest already runs, so the implementation cost is bounded.
RULING 2026-07-27 (gritzko): *the received pack is never read back and never even stored.* It is consumed once, forward, straight from the wire into the log. This supersedes the whole line of thought that preceded it (windowed io._mmap, an io._pread primitive, git-style dependency-ordered resolution) — none of it is needed, and none should be built.
Why it works: OFS_DELTA bases are backward-only (PackLog §Intra-pack object order, "forward-reference free"), so by the time a delta arrives on the wire its base has ALREADY been appended to our log. Every read-back therefore targets our own log, which is ≤2 GiB by the cap above and so maps with the plain whole-file io.mmap that exists today. The 6.33 GB pack is never seeked, never mapped, never written to disk. Resolving a chain is a walk inside a mapped log, exactly as it is today — no new access primitive, no traversal reorder.
The one new structure is a *pack-offset → log-offset map*, live only for the duration of one ingest. It is needed because rewriting a base ref changes a record's size (OFS distance re-encoded, or OFS→REF at +15 B), so log offsets drift away from pack offsets as you go. Two shapes, both measured against linux.git (11,690,992 objects, 3,566,506 of them — 30.5% — actually cited as a base; you cannot exploit that, since a base is named by an object that arrives later):
log_offset - pack_offset only where it CHANGES — at a crossing rewrite, and wherever accumulated drift pushes an OFS distance over a varint threshold. On the order of the 1,605,582 crossings → ~15 MB*. Same lookup, sparse.Start with the full map; compress to the drift table only if it bites.
The ingest loop is then: inflate → resolve against the log → sha1 → index → append with the base ref rewritten (OFS if the base landed in this log, REF if in an earlier one — its sha is known, we just indexed it) → rotate at KEEP_LOG_MAX. Constant RSS, disk cost = the logs alone (no 6.33 GB tmp spike on top).
Consequences to honour:
drainToFile, now feeding the ingest instead of a file writer.have-negotiate from what landed rather than re-downloading 6 GB. Git's protocol has no pack-transfer resume, so the tmp file never bought resumability anyway.[GET-044]'s drainToFile staging becomes unnecessary for the clone path; it is superseded here, not merely unused. Do not keep both shapes alive.Still first in line regardless: curlRun (be/shared/wire.js:250) buffers the entire https response in the JS heap, so it dies on any pack over the typed-array cap before ingest gets a chance to run. Streaming ingest cannot start until the bytes arrive AS a stream.
RULING 2026-07-27 (gritzko): the ingest loop is C, in libdog. *libdog sees the job as "repacking a git pack" and knows nothing about curl, HTTP, or pkt-line framing* — opening the stream, stripping headers, and consuming the negotiation preamble is JS-level routing that hands libdog a bare pack byte stream (an fd) and a destination shard, and gets back counts and the new refs.
The loop, per PackLog object order (forward-only, bases already behind us):
pack_off -> our_off to the ingest-local map (our off is a wh64 carrying the log/file number, so the entry is wh128b);hashlet -> our_off entry to the HIT index (needed to resolve REF entries);KEEP_LOG_MAX, start a new pack log.Why C and not JS: it is per-object over ~11.7M objects for a linux-scale clone, and every step is already native — dog/git/PACK.c header/ofs decode, PACKResolveOfs, PIDXScan (which already emits key=hashlet60|type4, val=offset wh128 entries straight into a caller-owned region), and abc's HIT/LSM for the index. In JS it would be 11.7M+ JSC binding crossings per clone, every one of them wrapping a buffer view and so exposed to the JAB-007 | 0 cursor hazard. JS keeps what it already owns: URL classify, capability/want-have negotiation, refs, shard resolution, rotate policy.
One correction to step 6, recorded because the instinct to drop it is reasonable and wrong: the running sha1 is *not* redundant with per-record hashing. Per-record hashes cover inflated object content only, not the framing bytes between records — and a stream cut that happens to land on a record boundary would otherwise parse as a clean, complete pack. The trailer is what distinguishes "peer finished" from "connection died tidily". It costs one pass over bytes already in hand.
dogrepack, verified on the 6.33 GB linux packBuilt 2026-07-27 in work/KEEP-006 as git/REPACK.cli.c (+ one add_executable line) — a libdog CLI running the exact loop above, so the design is measured rather than argued. It consumes an fd, never stores or maps the source, and resolves every base out of our own logs via the pack-offset map.
corpus objects result beagle 7,652 7,652/7,652 shas == git, 6 logs @4 MB cap, 234 REF git.git 417,182 417,182/417,182 shas == git, 3 logs @128 MB cap, 57,609 REF linux.git 11,690,992 11,690,992/11,690,992 shas == git, 3 logs @2 GiB cap,
2,198,136 raw / 7,886,869 OFS / 1,605,989 REF, 12m22s
Two independent confirmations fell out of it:
verify-pack data; the actual repack emitted *1,605,989* REF records — 0.025% apart. (git.git @128 MB: 57,855 predicted vs 57,609 actual, 0.4%; the gap is that predicted cuts fall on exact byte multiples while real log boundaries fall on record edges.) The +0.38%-of-pack figure for boundary re-anchoring stands.Three constraints the production ingest inherits, each found by hitting it:
avail_in is a 32-bit uInt*, so ZINFInflate refuses any window over 4 GiB. Handing it "everything buffered so far" fails at the FIRST record of a 6.33 GB pack. The reader must present bounded windows — which the u8b DATA span does by construction, since DATA can never exceed the buffer.ZINFInflate could not distinguish TRUNCATED from CORRUPT* — a starved stream yields Z_BUF_ERROR, damaged bytes yield Z_DATA_ERROR, and both collapsed into ZINFFAIL; PACKInflate then collapsed every ZINF error into PACKBADOBJ. Without that distinction a streaming reader cannot tell "refill and retry" from "give up", so the loop would have to guess a record size. Added ZINFMORE (git/ZINF.h/ZINF.c) plus the PACKInflate passthrough. Retry is safe because nothing is consumed until success and inflate has no side effects.Also settled by construction: the loop never needed io._pread, windowed io._mmap, or dependency-ordered resolution. Every one of the 9.49M deltas resolved through the map plus our own logs, including across closed-log boundaries. Record shape used: mean 541 bytes, max 1.6 MB compressed (git.git max 0.4 MB) — so a 1 GB buffer carries ~600x the worst record, and u8bReMap doubling is a safety net rather than a live path.
Buffer plumbing needs NO new primitive: abc/Bx.h already has it. PAST | DATA | IDLE maps one-to-one onto consumed / unparsed / read-target, with u8bShift (Bx.h:415) as drain-shift, u8bUsed as the parser cursor, u8bFed after a read, u8bMap for the MAP_NORESERVE allocation and u8bReMap for the grow case.
deepen-style rounds so no single pack nears the cap. Now an OPTIMISATION rather than a necessity: stream-ingest already handles an arbitrarily large pack. Still worth it for time-to-first-usable-state and for filter=blob:none. Yields thin packs whose bases are REF by construction — the same mechanism option 2 needs anyway.Both depend on REF_DELTA resolution, which is a RESTORE not a build — dog/git/PACK.c:252 returns PACKREF as a deliberate backstop ("Foreign REF packs go through UNPK"), and dog/git/PIDX.c:4 records that this leaner scanner DROPPED "the REF_DELTA waiter" the keeper had.
KEEP_LOG_MAX = 2^31-1 at the append/rotate site; an append that would cross it starts the next log. Cut wherever the cap falls — the flat crossing curve makes cut-point optimisation not worth building.curlRun buffers the whole body in the JS heap — port GET-044's streaming to the curl path. Blocks everything else.PACKResolve/PIDXScanRef take a caller finder (sha → base pack + offset), so a rotated log's cross-log bases resolve; no waiter forest needed, bases always land earlier.io._pread, windowed io._mmap, dependency-ordered resolution. Superseded by stream-ingest; recorded so nobody re-derives them.jab get, not a unit probe.The https clone path has its own unrelated wall that will mask any fix here: curlRun (be/shared/wire.js:250) buffers the whole response in the JS heap and dies on new Uint8Array(total) for a >4 GiB body (RangeError: length too large) — GET-044's streaming drainToFile was only wired to the spawn/ssh transport. Needs its own ticket; until it lands, https can't reach the log-writing code at all.
Filed 2026-07-27 from the linux.git clone benchmark; the read half of the rotation now works. UNCOMMITTED in the main tree per the no-post order.
jab/io.cpp JABC_MAP_MAX = 2^31-1 refuses oversized mappings in plain words (was an uncatchable SIGABRT past 4 GiB, and a silent Buf truncation from 2^31 up); corrected be/shared/wire.js header comment. Suggested commit: KEEP-006: refuse mappings over 2 GiB in plain words.PACKResolve + PIDXScanRef take a pack_ref_find (sha → base pack slice + offset) and resume the chase in whatever log answers; each hop carries its own pack. PACKResolveOfs/PIDXScan are the find == NULL wrappers, so the OFS-only PACKREF backstop is unchanged. dogscan <shard> reads a rotated shard back, its finder being just the entries the scan already emitted. Suggested commit: KEEP-006: resolve REF_DELTA bases across pack logs.dogscan resolves 417,182/417,182 objects in 69 s and every sha matches git verify-pack; beagle-ext at a 1 MB cap → 5 logs, 279 REF, same set as the single-log shard and as git. Unit: PACKRESOLVEtest cross-log chain (3 logs, plus finderless → PACKREF, unknown base → PACKREF), PIDXtest scan_ref; suite 108/108.