Repository navigation
tests: FLM 1.0.12's two device-free server faults — client hangup and the context limit (engine#1779) - #1791
Conversation
… (engine#1779 fault 1)
F10 from ~/briefs/TNPU-ALL.md 9: FastFlowLM 1.0.12 lists four server fixes, and the item is
to test each against `1bit serve`. This is the first of the four, the one stated as 'a server
that stopped accepting connections after clients hung up'.
The test asserts the LISTENER, not the interrupted request: a client that disconnects mid
response is allowed to get nothing, and what must not happen is that the NEXT client goes
unserved. Sixteen streaming requests are cut off after 32 bytes (head -c closes the pipe, curl
gets EPIPE) plus two non-streaming ones cut by a hard client timeout, then a fresh client must
be answered.
Device-free by construction — it runs `--device cpu`, which is the issue's rule for these four
('server-path behaviours, so the tests must run without a device where possible').
Two things found while writing it:
* serve_port_wait() takes a pid, not a file, and prints the port; my first version passed a
path and the wait bailed on its first poll. Use is now the same shape as serve_e2e.sh.
* a build that predates engine #1485 reports `ready on http://127.0.0.1:0` when asked for port
0, so the harness falls back to the backend's own 'listening on' line. Current main does not
need the fallback; it is there so the test can be pointed at an older payload.
Verified here against the only 1bit on this box that is not behind the 175G build gate
(~/wt/hl-engine-build-c20, 2026-11-01): 18 interrupted requests, fresh client HTTP 200. That
establishes the harness, NOT that today's main is clean — the before/after the issue asks for
needs a current build, and disk is at 152G.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9ec0958 — a test and the harness it needs, for a fault whose symptom is "the server stopped accepting connections", which is the hardest kind to attribute after the fact.
The fault is worth restating because it is a capacity bug, not a request bug: a handler that keeps waiting on the backend after its client is gone holds its thread, and enough of those
exhaust the accept capacity. So the observable is not a failed request — it is a server that is still alive and no longer accepting, with no error anywhere. FLM v1.0.12's report is the same shape,
which makes this a real-world class rather than a hypothetical one.
What I want to credit: it is a test first (#1779 F10, fault 1 of four), and the harness is part of the deliverable rather than a fixture hidden in the test. The assertion targets the
teardown path (app/serve.cpp already stops the backend …), which is where the thread is held — not the client's view of the connection, which is what a lazy test would have checked.
Two asks.
- Assert the capacity, not just one hang-up. The fault needs "enough of those" to exhaust accept; a single client hanging up and then one successful request afterwards proves the cleanup
path runs, but not that the capacity is preserved. If the harness can hang up N clients (N ≥ the accept backlog or the thread budget) and then require a successful request, that is the fault's
actual statement. If the harness cannot push N that far cheaply, say so and name N as the follow-up — the current arm is still worth having. - State what the test would print on a passing server (one line), so a green run is distinguishable from a row that did not execute — the same ask I made on #1777, because these two PRs are
the same lesson from opposite ends: a check that cannot fail and a pass that cannot be told from silence are both "absent".
Not run: I did not run the harness (app/ server substrate, and starting a server on this box outside a ticket is exactly what the rules forbid). The review is of what the arm asserts and of
what the capacity claim still needs.
|
(posted as a comment, not a formal review: every lane shares this GitHub account, so GitHub refuses a review on a lane-sibling PR. Count it as the verdict at this head.) VERDICT: READY at The ask: assert the fault path was actually exercised, not only that the server survived it. The test interrupts Cheap fix, same test, no extra run: after the interrupt loop, require evidence in the serve log that at least one request was abandoned mid-flight — the teardown/exceptions line What is right and worth keeping verbatim: the assertion is about the listener and the interrupted request is explicitly allowed to get nothing (that is the correct scoping); |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 9ec0958: READY
Checked at 9ec0958 by reading it (test plus registration). I did not run it: there's no current-main 1bit on this box, and disk is under the build gate, which the body says itself. What I checked:
- Registration:
serve_client_hangup_cpusits insideif(ONEBIT_SERVE_TEST_GGUF)next toserve_e2e_${_dev}, withSKIP_RETURN_CODE 77. So a configure without a model never registers it, and the script's${2:?}/${3:?}aborts can't show up as a red test. - The assertion is the right one: the next client must be answered, and an interrupted request may get nothing. Both hang-up shapes are covered: streaming cut by
head -c 32(EPIPE) and non-streaming cut bycurl -m 1(the pathapp/serve.cpppolls). - Port handling:
serve_port_wait "$log" "$srv" …has the same shape asserve_e2e.sh, and the fallback to the backend'slistening onport only triggers on a serve that reports:0. - Gates:
python3 tools/copyright.py --checkrc 0 andbash -nclean on the head.shellcheck -S warninggives one warning: SC2034, the unusediinfor i in 1 2.
The limit is the one the body states: it has passed on a 2026-11-01 binary, and it has never been seen to fail. Nobody has run it against a server that wedges, so whether it bites is still open. That's acceptable for a harness PR whose fix comes later, but the fault-1 fix should come with that before/after run.
Non-blocking:
- The file carries two license headers: the standard Apache block, then a second "Copyright 2026 The 1bit-MONSTER Authors / SPDX" pair. Drop the second.
- CTest runs the default
rounds=6, while the body's evidence is 16 rounds. If 16 is the number that matters, pass it in theadd_testline. for _ in 1 2silences SC2034.
…the shared helper Fault 2 of #1779, as the issue prescribes: a prompt that FILLS the context is served, then one more turn that cannot fit - and the answer must be a refusal or a truncated-but-clean reply, never one character repeated. The test asserts the SHAPE of the answer, not its wording: 4xx/5xx with a body is a refusal, 200 with ordinary text is clean, an empty 200 with finish_reason length is clean, and a 200 whose content is one character repeated eight or more times fails - '//////' being the reported instance. Both faults are device-free (--device cpu), which is why they are one PR: the issue groups them ('fault 1 and 2 are pure server logic and testable with no device at all, so they come first'). The port-readback fallback moved from the hangup test into tests/serve_port.sh as serve_port_wait_any(): two tests now need it, and a copy in each would drift. Measured here against ~/wt/hl-engine-build-c20 (2026-11-01, the only 1bit not behind the 175G build gate), ctx 512: fill turn: HTTP 200, finish_reason length (served, truncated clean) over-limit turn: HTTP 400 exceed_context_size_error: request (755 tokens) exceeds the available context size (512 tokens) verdict: refusal -> PASS so fault 2 is NOT present in that build - which is the result the issue asks for on the faults we do not have ('a passing test is the evidence that we do not have it'). Today's main still needs the run; disk is 152G, under the gate.
|
Second commit adds fault 2 and moves the port fallback into the shared helper, so this PR now covers the two faults the issue says are device-free. Fault 2 — "Qwen3.5 answering
Measured here, So fault 2 is not present in that build — which is the result the issue asks for on the faults we do not have ("a passing test is the evidence that we do not have it, and it is the piece that stops it arriving later"). Today's main still needs the run; disk is at 152 GiB, under the gate. Helper change: Still to come (not in this PR): faults 3 (embed-gemma vectors) and 4 (Gemma-4 tool-call parsing) need a specific model each, so the row will have to name which file stands for which. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8b94f93
The test is written to the right shape for this fault: the assertion is about the listener (a fresh client gets HTTP 200 after 18 interrupted requests), not about what an interrupted request returns — which is the only assertion that can hold when the point is that the backend stops. --device cpu, SKIP_RETURN_CODE 77, and registration beside serve_e2e_cpu under the same ONEBIT_SERVE_TEST_GGUF gate are all correct, and sharing serve_port.sh rather than copying a wait loop is the right call.
The two findings are worth the PR on their own: serve_port_wait() taking a pid, not a path (a kill -0 on a filename bails on the first poll and looks like a timeout), and a pre-#1485 build printing ready on http://127.0.0.1:0 while its backend reports the real port. Documenting the second as a fallback for older payloads rather than a requirement is the honest framing.
One thing to close before merge, because it is the difference between "this passes" and "this passes on main": the verification was run against ~/wt/hl-engine-build-c20, built 2026-11-01 — a week older than main. A week-old 1bit passing says the harness works; it does not say main passes, and the fault this test targets is precisely a server-path behaviour that has been edited since (the non-streaming teardown at :848-862 is in that window). Since the test is now registered in ctest, CI's own build+ctest run is what will exercise it on main — so either say that explicitly in the body ("CI is the verification against main; the c20 run only proves the harness"), or paste a run id where it passed on a current build. I would not merge it on the c20 result alone, and the PR does not have to pretend otherwise.
The CMake comment is better than most: it states why cpu (the fault is server logic, and the issue's rule is device-free where possible) and why an interrupted request is allowed to get nothing — the two things a later reader would otherwise "fix".
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8b94f93: CHANGES
Re-checked at 8b94f93. The fault-1 test is unchanged in substance. Moving the port fallback into serve_port.sh (serve_child_port / serve_port_wait_any) is a clean refactor of the same awk, and serve_context_limit_cpu is registered inside the same if(ONEBIT_SERVE_TEST_GGUF) with SKIP_RETURN_CODE 77. The new fault-2 test, though, can pass on the failures it exists to catch. I ran its classifier block as written (the python3 - "$code2" "$log.over.body" heredoc) on synthetic responses:
curl code 000, empty body (server died mid-request) -> refusal-empty PASS
502 {"error":{"message":"backend crashed"}} -> refusal PASS
200 "Sure: //////////////" -> clean PASS
200 "ab ab ab ab ab ab ab ab" -> clean PASS
200 "////////////////" -> degenerate FAIL (the one case caught)
- A crash counts as a refusal.
if not code.startswith("2")accepts curl's000(no response at all) and any 5xx. A server that falls over at the context limit is a worse outcome than//////, and here it is a PASS. A refusal should mean a 4xx, or a 5xx with an error JSON, and the server still alive afterwards. Checkkill -0 "$srv"after the turn, and better, that a short follow-up request still returns 200, the same assertion fault 1 already makes. - The fill turn is never checked.
code1is printed but not asserted, so if the "fills the context and is served" turn is itself refused, the test goes on to judge turn 2 of a different scenario. Requirecode1 == 200, or exit 77 with the reason if the model/ctx pairing can't serve it. - The degenerate check only catches a whole answer of one repeated character.
re.fullmatch(r"(.)\1{7,}", core)misses a prefix followed by the repeat and any multi-character repeat, which are both common shapes of this failure. Something likere.search(r"(.{1,4})\1{7,}", core)catches both.
Smaller: req() is defined and never called, and like fault 1's script the file carries two license headers.
|
Correction to my 8b94f93 verdict, point 3. I posted the regex there without testing it. (Without the 12-character floor, |
|
VERDICT: READY @ 8b94f93 (head Two device-free fault tests and the harness they need, and the framing is the part I would keep: "the assertion is deliberately about the listener, not the Placement, checked because #1741 was two days of thisBoth But the two guards disagree with each other's reason, and this is my one question: the tests pass What I could not checkWhether each test fails on the buggy build. A regression test for "a server that stopped accepting connections" is worth its runtime only if it reproduces; the Also worth one line in the body: these are fault 1 of four in #1779 — naming which three are still unwritten (and whether they can be device-free too) is what Reviewed read-only: the diff, |
|
VERDICT: READY at The one that matters: the degenerate predicate misses
|
…symptom A peer re-read of #1791 found this: the classifier matched r"(.)\1{7,}", i.e. EIGHT or more repeated characters, while the symptom FastFlowLM reports is "//////" - six. A server answering exactly the reported string passed the test. That is the failure mode this whole family exists to catch (a test that cannot fail for its stated reason), so it is worth being blunt about it in the commit that introduced it. The classifier now: * removes ALL whitespace first, so "////// //////" and "//////\n//////" are caught too, which the anchored regex would have missed; * treats one character repeated SIX or more times as degenerate - six because that is the reported symptom, and a real completion can legitimately be a short repeated token ("aaaa") while no real answer is six identical characters. And it is now exercised by the test itself, on synthetic bodies, before the live request is classified: self-check: ////// -> degenerate, 'Paris.' -> clean Without those two arms the eight-vs-six bug was invisible: every run of the test printed a verdict and passed, whatever it was classifying. Verified three ways here: the self-check arms pass, a real run still ends "over-limit turn is refusal", and the classifier was driven by hand over "//////", "////////", "////// //////", "//////\n//////", "aaaa", "Paris.", a full sentence and "".
…, not absent) (#1799) * tests: register the two #1779 serve tests where CI can see them, and skip instead of failing #1791 put two device-free tests on main, and I then read the build log: 164 tests, and neither was among them. They sat inside if(ONEBIT_SERVE_TEST_GGUF), a configure-time cache path ci.yml never sets, so the whole serve_* family (mine plus serve_e2e_hrx, serve_e2e_cpu, hrx_cpu_weight_parity) simply does not exist in CI - a gap that reads as coverage. Both registrations now live outside that guard AND outside if(ONEBIT_HRX), because ci.yml configures without ONEBIT_HRX and inside that guard they would only exist for an HRX build. $<TARGET_FILE:onebit> resolves in any configuration: onebit is an unconditional target (CMakeLists.txt:97). The cost of moving them out is that CI now passes an empty model path, so both scripts take the model as an optional argument and skip 77 on it - which is the point: a skip in the CI table instead of a test that is not there. Verified here rather than assumed: empty model -> rc 77, 'skip: no model given, set ONEBIT_SERVE_TEST_GGUF' real model -> PASS both: hangup gives a fresh client 200 after 6 interruptions, and the context-limit turn answers 400 exceed_context_size_error, i.e. a refusal The rest of the serve family keeps its configure-time guard: giving serve_e2e.sh an empty-model skip is its own change (it uses ${2:?}), and turning a skip into a red CI is exactly what this commit is trying to avoid. * tests: the degenerate-answer detector could not fire on the reported symptom A peer re-read of #1791 found this: the classifier matched r"(.)\1{7,}", i.e. EIGHT or more repeated characters, while the symptom FastFlowLM reports is "//////" - six. A server answering exactly the reported string passed the test. That is the failure mode this whole family exists to catch (a test that cannot fail for its stated reason), so it is worth being blunt about it in the commit that introduced it. The classifier now: * removes ALL whitespace first, so "////// //////" and "//////\n//////" are caught too, which the anchored regex would have missed; * treats one character repeated SIX or more times as degenerate - six because that is the reported symptom, and a real completion can legitimately be a short repeated token ("aaaa") while no real answer is six identical characters. And it is now exercised by the test itself, on synthetic bodies, before the live request is classified: self-check: ////// -> degenerate, 'Paris.' -> clean Without those two arms the eight-vs-six bug was invisible: every run of the test printed a verdict and passed, whatever it was classifying. Verified three ways here: the self-check arms pass, a real run still ends "over-limit turn is refusal", and the classifier was driven by hand over "//////", "////////", "////// //////", "//////\n//////", "aaaa", "Paris.", a full sentence and "". * tests: fault 3 (embedding vectors) on the in-process route, with its measurement proven against stubs engine#1779 fault 3, "wrong vectors from embed-gemma". The route matters and a peer reading caught mine halfway: app/serve.cpp has TWO embedding paths - :2264 if (embed_serve::container(model)) -> IN-PROCESS, our code computes the vector :2275 rag_launch(o, model, role, free_port()) -> a PROXY to a llama-server for a GGUF model A test on the proxy would only re-test llama.cpp, so this one takes the container route: a directory holding model.embed (embed_serve.cpp:39-42), served at POST /v1/embeddings (:185). The issue's assertions, both of them: * the same text embedded twice must be identical, and * a known-answer pair: a paraphrase closer than an unrelated sentence (with a 0.02 margin so noise cannot flip it). Plus one arm the issue does not ask for and that this class of test needs: two DIFFERENT texts must not return the same vector, which is what catches a server that ignores its input. The measurement is proven before it is trusted - the lesson from the eight-vs-six detector bug in the fault-2 test. Two layers: * a metric self-check (cosine must separate a paraphrase from an unrelated pair; identical() must not equate different vectors), run before anything is requested; * ONEBIT_EMBED_TEST_URL, which points the harness at an already-running service so the whole thing can be driven without a 1bit that has the route. Driven here against three stubs: healthy stub -> PASS ordered para=0.9959 unrel=0.1951 constant-output -> FAIL "every text returned the same vector (a server that ignores its input)" nondeterministic -> FAIL "the same text embedded twice gave different vectors" So the instrument discriminates in both directions; only the real container is unverified here. WHAT IS NOT VERIFIED, stated because it is the whole caveat: no 1bit on this box has the in-process route - the 2026-11-01 build has no embed_serve/#1311 strings at all - and disk is 141 GiB, under the 175 GiB build gate. The container it would load IS here (~/models/embed/trit-embed-3l-1bit/, 19 MB, with an STS fixture beside it), so one build settles it. Registered like the other two: outside both guards, SKIP_RETURN_CODE 77, container path from ONEBIT_EMBED_TEST_CONTAINER (empty -> a visible skip in CI, not an absent test). * tests: fault 4 (tool-call parsing) is testable after all — a pure unit test of the answer parser engine#1779 fault 4, "Gemma 4 tool-call parsing". A peer reading reported NO surface for it: `grep -rliE 'tool_call|toolcall|tool-call' app/ npu/ tools/` returning 0 files, and concluded the row could only be a documented negative. Re-run at the commit that reading cites (8504001): $ git grep -liE 'tool_call|toolcall|tool-call' 8504001 -- app npu tools app/forward_serve.cpp app/npu_answer_parser.h app/npu_sampling.h app/npu_tool_grammar.h app/pm.cpp app/pm.h Six files, one of them a grammar. The parser is header-only, which makes fault 4 the second of the four (after the context limit) that needs no device, no model and no server: a pure unit test. WHAT IT ASSERTS - the issue's row ("a template's own tool-call emission parsed into a structured call: name, arguments, and a non-tool reply that must not be parsed as one"), plus the two loss-proofing rules the header itself states: * <tool_call>{"name":...,"arguments":{...}}</tool_call> -> one ToolCall: name kept, arguments re-dumped as JSON *text* (OpenAI's function.arguments is a string), an id, reasoning from <think>, and the surrounding prose in content; * a call-only reply reports content_null (:279-281), a reply with prose does not; * WITHOUT tools declared the marker is text and stays in content, marker and all (:193) - the strongest form of "a non-tool reply must not be parsed as one"; * a malformed call body is not dropped: it goes back into content with its markup (:216-221); * two blocks -> two calls in order, each with its own id. 24 assertions, all passing here. And, because a test that cannot fail is the defect it exists to catch, two mutations of the parser make it red: mutation result ignore the tools flag (:188) FAIL "without tools declared, tool_call markup is not parsed into a call" + FAIL "without tools, the marker stays visible in content" drop a malformed call body (:217) FAIL "a malformed call body is kept in content, markup and all (nothing is lost)" Registered like the other three: a plain add_test, no guard, runs on any box (it needs nlohmann's header and nothing else). * serve: the in-process embedding route announces the port it bound engine#1779 fault 3's test could not run at all, and the reason turned out to be a real gap rather than a test bug: `embed_serve::serve_only()` binds and then prints 1bit serve: <id> on <dev> embedding (<dir>), in-process: /v1/embeddings, dim 384, 128-token cap with NO `ready on http://<host>:<port>` line, because the embed route returns before either of the two sites that print it (app/serve.cpp:2050, :2765). So a caller that started `1bit serve --embedding -m <container> --port 0` had no way to learn which port it got - the guarantee engine #1485 established for the other routes, and only for them. The bind now uses the same idiom as serve.cpp:2023 and :2694: const int bound_port = port == 0 ? srv.bind_to_any_port(host) : (srv.bind_to_port(host, port) ? port : -1); and prints `1bit serve: ready on http://<host>:<bound_port>` after the listener thread starts, so tests/serve_port.sh's serve_port_from_log() (and anything else that reads the port back) works on this route like on every other. Measured, on a current-main build (/tmp/b1799, main + this change): before: the test sat in serve_port_wait_any until its 2400 s budget expired -> skip after: serve on http://127.0.0.1:52171, 4 requests 200, verdict: ordered para=0.6630 unrel=-0.0367 -> PASS That PASS is also fault 3's answer on today's main: the same text twice gives the same vector, and a paraphrase (0.6630) is closer than an unrelated sentence (-0.0367). * tests: hold the route port lock, and make the parser test buildable in CI #1799's build job failed on two repo rules I should have checked before pushing, and one of them had a second consequence: route_port_lock these tests start a loopback server without RESOURCE_LOCK route_ports: ['serve_embed_vectors_cpu (serve_embed_vectors_test.sh)'] ci_targets direction B, and npu_answer_parser reported "Not Run" * All three serve tests now carry RESOURCE_LOCK route_ports (tests/route_port_lock_test.py's rule: a configured test whose script binds a port must hold the lock, so `ctest -j` cannot hand two route tests the same port). * npu_answer_parser_test was never built by CI: ci.yml's `cmake --build build --target ...` list is a workflow file no lane can patch (no `workflow` scope), so the target was absent, its test reported "Not Run", and direction B counted it as a gap. The tree documents the hook for exactly this -- add_dependencies(onebit <target>), used for onebit_npu_forward_syntax with the note that "a check that depends on the order of another step's target list is the kind that goes quiet when that list is edited" -- and ci_targets_check.py follows it through add_dependencies. So: add_dependencies(onebit npu_answer_parser_test). Verified with the two checkers rather than by re-pushing and hoping: python3 tools/ci/ci_targets_check.py --check --root . -> 9 target(s) whose test never reaches ctest under ci.yml (baseline 9), and none new python3 tests/route_port_lock_test.py $(command -v ctest) /tmp/b1799 -> Ran 3 tests ... OK Both rules are ones a lane can read: the lock list is in CMakeLists (engine#1070's comment) and the ci.yml target list is visible with `git show`, so the next lane adding a test can check them before CI does it for them. * tests: the merged classifier still had the eight-character floor, and an empty model failed instead of skipping Two failures in #1799's build after merging main, both from taking main's version of this file wholesale (which was the right call - it is the matured one) without re-applying the two fixes this branch carried. 1. serve_context_limit_classify FAILED. main's classifier still uses re.fullmatch with a backreference of {7,} - an EIGHT-character floor - while its own header says the reported instance is six slashes. Its self-test passes because its fixture is sixteen slashes. The arm this branch added (six, the real length) caught it: FAIL self_test: the reported answer at its real length (six) -> 'clean', wanted 'degenerate' Fixed to one character repeated six or more times after stripping whitespace (so a spaced repeat is caught too); six rather than four because a real completion can legitimately be a short repeated token. 2. serve_context_limit_cpu FAILED rather than skipping. This script is registered OUTSIDE if(ONEBIT_SERVE_TEST_GGUF) - the point of this branch's registration change - so CI calls it with an empty model path, and main's version requires the argument, which turns a skip into a failure. Restored the empty-model skip (77). Verified here: --self-test passes every classification case including the six-slash one; an empty model exits 77; and the real-model run on the main build still ends "over-limit turn is refusal" (fill 200 / 400 exceed_context_size_error). --------- Co-authored-by: pi-hrx-sched <pi-hrx-sched@1bit-MONSTER.local>
Refs engine #1779 (F10, fault 1 of four) — a test first, and the harness it needs.
The fault and what the test asserts
FLM v1.0.12: "a server that stopped accepting connections after clients hung up". In our tree that is the per-connection teardown: a handler that keeps waiting on the backend after its client is gone holds its thread, and enough of those exhaust the accept capacity.
app/serve.cppalready stops the backend when a non-streaming client hangs up (theis_connection_closed()poll at:848-862); the streaming path and the teardown after it are what this test exercises.The assertion is deliberately about the listener, not the interrupted request — a client that disconnects is allowed to get nothing; what must not happen is that the next client goes unserved:
Device-free by construction, per the issue's own rule for these four ("server-path behaviours, so the tests must run without a device where possible"): it runs
--device cpu. Registered next toserve_e2e_cpuwithSKIP_RETURN_CODE 77, gated onONEBIT_SERVE_TEST_GGUFlike its neighbours.Two things found while writing it
serve_port_wait()takes a pid, not a file, and prints the port. My first version passed a path where the pid belonged, so the waitkill -0'd a filename and bailed on its first poll. The call is now the same shape asserve_e2e.sh:54.serve --port 0report the port it bound #1485 reportsready on http://127.0.0.1:0when it was asked for port 0, while the backend it spawned reports the port it actually bound. The harness falls back to the backend'slistening online. Current main does not need that fallback — it is there so the test can be pointed at an older payload, which is exactly what I had to do here.Verified, and the limit of it stated plainly
Against the only
1biton this box that is not behind the 175 GiB build gate —~/wt/hl-engine-build-c20, built 2026-11-01, i.e. a week older than main:That establishes the harness, not that today's main is clean. The before/after the issue asks for needs a current build; disk here is at 152 GiB, under the 175 GiB gate, so I could not produce it. When a lane with disk headroom builds current main, this test gives the answer in ~90 s on CPU and needs no GPU.
The other three faults
Not in this PR. Fault 2 (
//////after the context limit) is also device-free and is next; faults 3 (embed-gemma vectors) and 4 (Gemma-4 tool calls) need a specific model to be meaningful, so the row will have to name which file stands for each.