docs(MODEL-MM-dots3-note): W0.5 — Thor is a CUDA host now, and its baseline caught a segfault on main (#699, #955, #960) - #956
Conversation
…seline caught a segfault on main (#699, #955, #960) Option B of the dots3-note spec §6.4 needs somewhere our own arm can run, and Thor at 192.168.68.23 is the only non-GB10 CUDA box we have. It had no CUDA toolkit, no cmake and no nvcc, and it cannot get them: `/` is a read-only 4.4 G ext2 loop on an immutable Kairos image with 1.3 G free, so `apt install` into it does not exist as an option. The toolchain therefore lives in a container pinned by DIGEST — the `13.0.1` tag moves — and `/home` (362 G free) holds the checkout. The three flags that make the GPU visible are `--runtime=nvidia`, `-e NVIDIA_DISABLE_REQUIRE=1` and `sudo -n`. The box's own `/home/mudler/_build_thor.sh` asserts the runtime "REFUSES this image on its driver" and omits `--runtime` on that basis; it is wrong, and only because it also omits the DISABLE_REQUIRE flag. With all three, `cudaGetDeviceCount` returns 1 and an `nvcc -arch=sm_110` kernel launches and returns correct values. Verified rather than assumed, because a build that quietly disables CUDA is the failure mode here: configure prints `CUDA target architectures: 110`; all 30 `*.cu.o` contain exactly one cubin each and every one is `sm_110`; `libvllm.so` links `libcudart.so.13`. 30 of 53 `.cu` files is CORRECT — `fa2`, `cutlass-nvfp4`, `cutlass-fp8`, `scaledmm-c3x-*` and `fp4-mma` all resolve DISABLED for `[110]` and only `marlin-nvfp4` is ENABLED, so the feature table narrows the TU set on purpose. On the device, `test_cuda_backend` reports `sm_110`, `integrated=1`, `UnifiedMemory=true`, 25/25. THE BASELINE MOVED WHILE THIS PR WAS OPEN, AND THAT IS THE HEADLINE. Measured at 5a0ffe9 it was 484 tests / 14 red. Re-measured on the same box in the same container after rebasing onto 2daa328 it is 485 tests / 15 red, and the FP8 group changed CHARACTER: `vt: no kernel for op QuantFp8Static (id 52) on device cuda` — a loud, correct refusal — became `[vt reference-tier] ... running the PORTABLE CPU fallback` followed by SIGSEGV. Five clean refusals became five crashes and #842's new `test_ops_fp8_cpu` added a sixth, crashing at :279 in the case named "G2: CPU QuantFp8Static equals CUDA QuantFp8Static byte for byte". `cutlass-fp8` is ENABLED on GB10 and DISABLED for [110], so the native kernel exists on the gate host, the fallback is unreachable there, and nothing in CI could see it. Filed as #960; not fixed here, because it is someone else's kernel row and wants its own spec. The rest of the 15 are recorded as #955, the sm_110 counterpart of #907, and nothing is fixed among them either — knowing which tests are red on sm_110 IS the deliverable. Four throw `built without the vendored FlashAttention-2`, two hardcode GB10 (`test_platform` asserts capability family 120; `test_op_parity` replays a dgx-captured hash in a case that names itself dgx-only and runs anyway), and `test_capi`/`test_linear_method` are already red on GB10. The one substantive standing finding is `test_ops_moe_grouped` at bitdiff=15/32768 on a Marlin NVFP4 cell that IS enabled for [110] — a live kernel disagreeing with itself across block sizes, not an absent feature. So the spec's W0.5 gate as written — "the existing suite passes there" — is not met and was the wrong gate: it would either block every brick forever or invite someone to weaken a test. The spec now carries the replacement and the reasoning for it, so a future agent reads it there rather than in a PR body: a row regresses on Thor only if it lengthens the list. And because the list is not constant, the spec also says to re-measure whenever the base moves across `src/`, `tests/` or `CMakeLists.txt`. One trap cost a false red. `test_serve_low_tools` failed the first run with `FileNotFoundError: 'shellcheck'` — `tests/tools/test_online_gate_startup.py:260` shells out and raises instead of skipping when the binary is absent, an absent instrument reading as a code verdict. `shellcheck` is in the recorded Dockerfile so it passes here, and the record says plainly that this HIDES the harness defect rather than fixing it. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
ba4ed0c to
b1ae135
Compare
|
Rebased onto The rebase pulled 37 files across
The FP8 group did not just get worse, it changed character: the loud refusal became Filed as #960, not fixed here. Attribution to Records updated to the new base throughout: Spec reconciled by section, not by conflict marker. The differential gate reasoning now lives in the spec, not only here (§7's W0.5 entry): all-green is the wrong bar on a host whose arch legitimately lacks features, because it would either block every brick forever or invite someone to weaken a test; a row regresses on Thor only if it lengthens the list. The spec also now says to re-measure whenever the base moves across
|
…W6's own variable shadow (#965, #672) `windows-msvc-cpu` and `windows-msvc-vulkan` fail on every open pull request: server_main.cpp(1315,55): error C2220: the following warning is treated as an error server_main.cpp(1315,55): warning C4456: declaration of 'loaded' hides previous local declaration That is W6's own speech-attach block declaring `loaded` inside the scope of the text engine's `loaded` at `:1025`. It is the ONLY warning in the job, it has been on `main` since W6 landed, and it is fixed here by renaming the inner declaration. Nothing is suppressed and no detector is weakened. WHAT FOUND IT WAS THE MATCHED-ARM CHECK, NOT THE LABEL, and that is the part worth recording. Both jobs are habitually red and habitually attributed to #645 — which is the `M_PI` portability regression in three LTX2 sources: different file, different detector, different failure. A second cause sitting behind a known-red name is invisible for exactly as long as nobody reads the log. Three unrelated open pull requests that touch no speech surface — #956, #950 and #939 — fail with the identical C4456, which is what separates "pre-existing" from "mine". `main` carries no baseline because `windows-msvc-*` are PR-only (#584), so the failure presents to each author in turn as a red their own diff caused. Verified after the rename: 7 of 7 server ctest cases pass, `test_openai_api_server` is 62 cases / 733 assertions, and `vllm-server --speech-model <dir>` with no `--model` starts and serves for real: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') Issue: #965 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
…the first, and it came from main (#968, #672) With #965's `C4456 'loaded' shadow` removed, `windows-msvc-cpu` and `windows-msvc-vulkan` failed again on this row's pull request — and on a different cause: include\vector(1461,29): error C2220: the following warning is treated as an error include\vector(1461,29): warning C4244: '=': conversion from 'const double' to 'float', possible loss of data raised from `src/vllm/multimodal/ltx2_video.cpp:203,214`, the two narrowing `positions.assign` calls that `c7cb59fbb` (#964, LTX25-TOKEN-APPEND) landed on `main` while this row was in flight. `StreamState::positions` and `Ltx2LatentState::positions` differ in element type; GCC and Clang narrow silently, MSVC diagnoses and the build treats it as an error. NOT FIXED HERE, deliberately. #964's own comment at `ltx2_video.cpp:129-132` reasons that "double -> float -> double reproduces the bits", so the narrowing is intentional and a silencing `static_cast` would be a claim about that reasoning rather than a formatting repair. It belongs to the lane that owns the round trip. Filed as #968 with the evidence rather than papered over. THE MATCHED ARM SPLITS EXACTLY ON THE MERGE BASE, which is what makes it inherited rather than mine. Grepping each `windows-msvc-cpu` job log for the warning: #966 and #951, both on `c7cb59fbb`, hit it twice each; #967, #956, #950, #939 and #938, all based before it, do not hit it at all. This row's diff touches zero LTX2 files. THE FINDING WORTH CARRYING is not either warning. It is that TWO INDEPENDENT CAUSES WERE STACKED BEHIND ONE HABITUALLY-RED JOB NAME, and the first hid the second — and that neither was #645, the `M_PI` regression both jobs are usually attributed to. A known-red list tells you a job is often red. It never tells you that today's red is the same one. Only reading the log does. Issue: #968 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
… heard, and the five keys upstream refuses that we dropped (#672, #953, #965) (#966) feat(MODEL-MUSIC-MUSIC3): a music-only server, an example that can be heard, and the five keys upstream refuses that we dropped (#672, #953) Developer directive: parity on what upstream supports — "we want to be a good reference" — usage docs for MiniMax-Music3, and in those docs the weights. Then, mid-flight: "we should allow to load only the music model" and "we need to have an e2e test working". FOLLOWING_AGENTS_PROTOCOL ## The upstream surface, enumerated rather than summarized SGLang-Omni `748a0b43` `models/minimax_music3/` and diffusers `c6da9936` `modular_pipelines/minimax_music3/`, read field by field and recorded with `file:line` in spec §10.1 so the next reader re-derives nothing. **Closed here:** the music-only server, the missing example, and five refusals. **Owed and named:** the non-`wav` response formats, request batching and `/v1/audio/speech/batch`, the 32 kHz delivery resample. **Permanently refused rather than owed:** streaming — neither upstream arm has it (`supports_streaming_vocoder=False`). **One place we are ahead of both arms:** `guidance_scale` is a real per-request control here, where diffusers freezes it at 1.7 into the guider component (`denoise.py:180`) and SGLang exposes it only as a serve-time knob. ## `--model` is optional when `--speech-model` is given Serving a 28.5 GB music model also forced loading an unrelated text model, and on this box the smallest text checkpoint is 35B — so the recipe this project documented was effectively unrunnable. Upstream's own is `sgl-omni serve --model MiniMaxAI/MiniMax-Music3`, no text tower anywhere. vllm-server --speech-model /path/to/minimax-music3 Third instance of a shape already in `server_main.cpp`: a pooling checkpoint serves `/v1/embeddings` alone, a Parakeet checkpoint serves `/v1/audio/transcriptions` alone. It mirrors vLLM's task-conditional registration (`api_server.py:255-265`). **Additive, and proved rather than argued.** The only case whose verdict changes is BOTH flags absent, which was an error and remains one with a message naming both options. The route table is gated in both directions over a real socket, because a handler-dispatch test cannot see route registration at all. ## The example the music family did not have `examples/minimax_music3_gen` — a thin client of `include/vllm.h` and nothing else, like `parakeet-transcribe` and `vllm-cli`. Hearing this model previously needed a running server plus a `curl`, or a C ABI caller nobody had written. ## Five keys upstream refuses by name were SILENT here (#953) `temperature`, `top_p`, `top_k`, `repetition_penalty` — refused upstream at `request_builders.py:14-19,109-114`, because this model's autoregressive stage has ONE sampler, a fixed top-50 draw (`encoders.py:48,94-103`). And `max_new_tokens`, upstream's LENGTH spelling in 25 Hz frames rather than seconds (`request_builders.py:56-68`), so a 250-frame request silently became the family's 60 s default. That is the #925 class exactly, in the same file that already carries #925's refusal one paragraph above. Fixed in flow. ## The e2e gate no longer reports a skip wearing a pass It read 5 cases / 5 passed with **`assertions: 0`** whenever the checkpoint was absent — the same shape that fooled this project on `test_qwen3_paged_engine`. Split into a checkpoint-free half that runs unconditionally in CI (request contract, both ceilings, the speech-only route table over a real socket with a stub synthesizer) and the env-gated half, whose HTTP case now drives the real engine over a real socket against the music-only server shape. A coverage-report case prints which arms ran, every run. The full arm was run: `POST /v1/audio/speech -> 200 audio/wav, 12332 bytes in 518 s wall`, 2 AR frames -> 6 latent frames -> 3072 samples/channel, 6144 int16 samples all non-zero, 0 clipped, 2818 of 3072 positions differing between L and R, and `/v1/completions` + `/v1/chat/completions` both 404 from the route table. `checkpoint_arms_run=5`. | arm | cases | assertions | |---|---|---| | `test_minimax_music3_e2e_real`, no env vars | 9 | 37 (was 5 / **0**) | | `test_minimax_music3_e2e_real`, checkpoint only | 9 | 86 | | `test_minimax_music3_e2e_real`, checkpoint + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | | `test_speech_api` | 6 | 67 | | `test_openai_api_server` | 62 | 733 | | `test_openai_conformance` | 23 | 252 | | `test_minimax_h3` (unchanged) | 79 | 57395 | | server flag ctest cases | 7 passed | | ## The weights are documented (porting-a-model.md §2.1) `docs/USAGE.md` gains component-by-component tables: the diffusers arm at `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, **28.5 GB resident** (28 517 617 303 B, measured) out of a 57.4 GB repository and why they differ; the native `.pth` arm we refuse and SGLang-Omni serves; the one implemented GGUF Q4_K artifact with its sha256; and the fourteen third-party quantized repositories in five formats, each marked refused. The revision is verified rather than copied — `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to that revision's own LFS record. ## A sample a human can hear 2.0 s of 44100 Hz stereo from this engine in 3286 s of wall clock: RMS 0.03169, peak 0.97437 with 0 clipped samples, 84 073 of 88 064 positions differing between left and right. **Its samples are compared to nothing** — §5 withdrew the token gate — so it shows the pipeline runs, not that the music is right. It is not committed: `check-pr-size.py` classifies every path and none takes a `.wav` outside `tests/`, where a file compared to nothing would sit beside the goldens and imply it was one. ## The four asks, answered directly **1. Music-only server.** `vllm-server --speech-model <dir>` with NO `--model` starts and serves, observed live rather than inferred: server: speech/music-only model (family=minimax-music3, 44100 Hz, text-only synthesis, family DETECTED); serving /v1/audio/speech server: listening on http://0.0.0.0:18923 (model 'minimax-music3') `--model` alone and `--model` + `--speech-model` are **byte-identical in behaviour**. The whole change is one new early branch, `if (args.model_dir.empty())`, which loads the speech engine and `return 0`s before reaching a single line of the existing path; nothing downstream of it was touched. The only case whose verdict changes is BOTH flags absent, which was an error and remains one. Server suites: **7 of 7** ctest cases (4 pre-existing + 3 new — neither flag is still an error and now names both options; `--speech-model` alone reaches the speech LOAD; `--speech-family` alone still demands a checkpoint), `test_openai_api_server` **62 cases / 733 assertions** (+1 case / +24 assertions, the speech-only route table over a real socket), `test_openai_conformance` **23 / 252** unchanged. **2. e2e, three arms.** What a bare CI run executes unconditionally: the request contract on the exact body the real case posts, the near-miss and sampling refusals, the duration arithmetic including both ceilings, and the speech-only route table over a real socket with a stub synthesizer. What stays env-gated: everything needing the 28.5 GB checkpoint, plus the two 2.4B-DiT arms behind `VLLM_CPP_MUSIC3_DIT`. | arm | cases | assertions | checkpoint arms run | |---|---|---|---| | no env vars (what CI runs) | 9 | **37** | 0 — was 5 cases / **0 assertions** | | `VLLM_CPP_MUSIC3_CHECKPOINT` | 9 | **86** | 3 | | + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | 5 | **3. The five keys.** All five were **accepted and silently dropped**; all five are **now refused by name**. | key | upstream anchor | why it cannot be honoured | |---|---|---| | `temperature` | `request_builders.py:14-19,109-114` | the AR stage's only sampler is a fixed top-50 draw, `encoders.py:48,94-103` | | `top_p` | same | no nucleus branch exists | | `top_k` | same | `_AR_SAMPLING_TOP_K` is a module constant of 50 | | `repetition_penalty` | same | no penalty is applied anywhere in the loop | | `max_new_tokens` | `request_builders.py:56-68`, `constants.py:4-5` | upstream's LENGTH, in 25 Hz frames not seconds; the refusal names `audio_duration` and the /25 conversion | **4. The weights table** (`docs/USAGE.md`, "MiniMax-Music3: the exact weights"). It carries: repo **and** revision — `MiniMaxAI/MiniMax-Music3`@`fbdf52fbaaca799592917417eb05f1899f1255ec`, verified rather than copied, since `condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to `83179c5e…a202c2a4d`, that revision's own LFS record; the Q4_K artifact's sha256 `4c5d41b2…c70cbdd0` at revision `c36aaeed…` with its exact byte count; **28.5 GB resident (28 517 617 303 B, measured) versus 57.4 GB repository**, with the reason they differ; the refused native `.pth` arm (`qwen_7B/`, `flowmatching_vae.pth`, `dav.pth`) and that SGLang-Omni serves it; and all fourteen community quant repositories across five formats, each marked refused and each marked **third-party** rather than first-party. This is the first application of `.agents/porting-a-model.md` §2.1 (landing as #951). ## Two reds stacked behind one habitually-red job name `windows-msvc-cpu`/`windows-msvc-vulkan` are usually attributed to #645 (`M_PI` in three LTX2 sources). **Neither of the two causes here was #645**, and the first hid the second. **#965, fixed in flow.** `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — W6's own speech-attach block declaring `loaded` inside the text engine's `loaded`. The only warning in the job, on `main` since W6 landed. Matched arm: #956, #950 and #939, none touching the speech surface, fail identically. Renamed; nothing suppressed. **#968, filed and NOT fixed here.** With the shadow gone the same jobs failed again on `C4244: conversion from 'const double' to 'float'`, raised inside MSVC's `<vector>` from `ltx2_video.cpp:203,214` — two narrowing `positions.assign` calls that `c7cb59fbb` (#964) landed on `main` while this row was in flight. **This branch touches zero LTX2 files.** The matched arm splits exactly on the merge base: #966 and #951 (on `c7cb59fbb`) hit it, #967/#956/ #950/#939/#938 (before it) do not. It is deliberately left to the LTX-2.5 lane — #964's own comment reasons that "double -> float -> double reproduces the bits", so a silencing cast is a claim about that reasoning rather than a formatting fix. **The finding, which outlives both:** a known-red list tells you a job is often red. It never tells you that today's red is the same one. Only reading the log does — and here it took two readings, because removing the first cause is what made the second visible. <!-- kept for the record --> ### The first of the two, in detail (#965) `windows-msvc-cpu`/`windows-msvc-vulkan` failed here, and they are **not** #645 (`M_PI` in three LTX2 sources). They were W6's own `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — the only warning in the job, on `main` since W6 landed. The matched-arm check is what separated it from my diff: #956, #950 and #939, all touching no speech surface, fail identically. `main` has no baseline because `windows-msvc-*` are PR-only (#584), so it presents to each author in turn as their own red. Filed and fixed in flow by renaming the inner declaration; nothing suppressed. ## Mutations Four run, four fire: sampling refusal neutered (5 assertions red), `max_new_tokens` refusal neutered (2 red), `--model` made mandatory again (2 ctest cases red), generate routes registered unconditionally (3 cases / 6 assertions red in the api-server suite, 1 / 2 in the e2e suite). Sources restored and verified sha256-identical. Supersedes #954 (untrailered merge commits) and #963 (a `server_main.cpp` commit that owed `docs/USAGE.md` under the per-commit documentation checkpoint). Same tree, linear history, every commit green on `check-doc-checkpoint`, `check-commit-trailers` and `check-commit-style` locally before pushing. Every source file is byte-identical to the one built and gated. Issue: #672 Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
W0.5 of the
dots3-noterow (#699), the first dispatchable task under §6.4 option B. Docs and records only — nosrc/, noinclude/, notests/. Issues: #699 (the row) and #955 (the sm_110 baseline this opens).What was done on Thor
192.168.68.23now builds vllm.cpp with CUDA ON for sm_110, runs kernels on the device, and has a recordedctestbaseline. The recipe is in.agents/environment.mdso the next agent does not repeat the archaeology.Container, not a host toolchain, and the reason is measured.
/on Thor is a read-only 4.4 Gext2loop (/dev/loop0, 1.3 G free) — it is an immutable Kairos image, soapt installinto/is not available at all. A/home-prefix CUDA runfile would work but has to be re-derived after every reimage; the container carries the toolchain in one pinned digest and the NVIDIA runtime injects the host driver.The image is a 4-line Dockerfile on
nvidia/cuda@sha256:7d2f6a8c2071…(pinned by digest — the13.0.1tag moves) pluscmake ninja-build git python3 python3-dev ca-certificates shellcheck. Inside: nvcc 13.0.88, cmake 3.28.3, ninja 1.11.1. Every invocation needs--runtime=nvidia,-e NVIDIA_DISABLE_REQUIRE=1andsudo -n.That last point corrects a script already on the box:
/home/mudler/_build_thor.shstates the container runtime "REFUSES this image on its driver" and drops--runtimeon that basis. It is wrong, and it is wrong only because it also dropsNVIDIA_DISABLE_REQUIRE=1.Proof the build is really a CUDA build
A build that quietly disables CUDA is the failure mode this task exists to catch, so it is checked three independent ways:
CUDA target architectures: 110and resolves/usr/local/cuda/bin/nvcc(NVIDIA 13.0.88);*.cu.oobjects, each containing exactly one cubin, allsm_110—cuobjdump --list-elfover every object reads30 sm_110. 30 of the tree's 53.cufiles is correct, not partial:fa2,cutlass-nvfp4,cutlass-nvfp4-sm100,cutlass-fp8,scaledmm-c3x-sm90,scaledmm-c3x-sm100andfp4-mmaall resolve DISABLED for[110], onlymarlin-nvfp4is ENABLED, and configure says the TU set is narrowed byVT_CUDA_FEATURE_TABLE. CUTLASS is absent and supplying it changes nothing — the cells are arch-gated, not CUTLASS-gated;ldd libvllm.soresolveslibcudart.so.13andlibcublasLt.so.13.Runtime, on the device:
test_cuda_backendreportsCUDA compute capability: sm_110andpageable=1 integrated=1 UnifiedMemory=true, 6/6 cases and 25/25 assertions. A hand-writtennvcc -arch=sm_110kernel also launches and returns correct values.Build was
-j4on purpose (vm.overcommit_memory=1, zero swap, no OOM killer — the box reboots instead). It cost nothing: 1460/1460 targets in ~20 min at peak ~6 GB of 122 GB.The baseline — 14 red, and that is the deliverable
ctest -j4 --timeout 1800at5a0ffe9e3: 484 tests, 468 passed / 2 skipped / 14 FAILED, 177.89 s. All 14 reproduce at-j1, so none is a starvation artefact. Nothing was fixed; the full table with per-test first-failing assertions is in.agents/environment.mdand in #955.They collapse into four causes. Ten are the build honestly refusing what sm_110 lacks — five throw
vt: no kernel for op QuantFp8Static (id 52), four throwbuilt without the vendored FlashAttention-2on MLA prefill. Two hardcode GB10:test_platform:307assertsis_device_capability_family(120), andtest_op_parity:2487replays a dgx-capturedoutput_cbor_sha256inside a case that names itself dgx-only and runs anyway.test_capi's SIGSEGV andtest_linear_method's un-run MXFP4 fused path are already red on GB10 (#907).The one substantive finding is
test_ops_moe_grouped:1144:NVFP4 block8-vs-block16 M=8 K=4096 N=4096 bitdiff=15/32768.marlin-nvfp4IS enabled for[110], so unlike the other groups this is a live kernel disagreeing with itself across block sizes on hardware that runs it. Possibly adjacent to #325 (same cell, same device) but that is a throughput claim and this is a correctness one.Two things stated plainly rather than worked around
The spec's W0.5 gate is not met. It read "the existing suite passes there", and it cannot: ten of the 14 reds are feature absence this row cannot make green. The spec now says so and replaces it with the gate that actually binds — a row regresses on Thor only if it lengthens the list of 14.
One false red was mine, briefly.
test_serve_low_toolsfailed the first run withFileNotFoundError: 'shellcheck':tests/tools/test_online_gate_startup.py:260shells out and raises rather than skipping when the binary is absent — an absent instrument reading as a code verdict. That was a property of my image, not of Thor.shellcheckis in the recorded Dockerfile and the test passes; the 14 above are with it installed.Also corrected in the records
nvidia-smion Thor was never broken — it works undersudo -nand fails only unprivileged, soNvRmMemInitNvmap failed: Permission deniedwas a privilege problem. Its memory columns read[N/A]because this is an integrated GPU with no VRAM counters, so readfree -ginstead.~/gpu.lockexists again (the 2026-08-11 note saying it does not is stale) and a hostlocal-ai workeris back.Verification
scripts/agent-preflight.sh --stagedis green on every record gate that applies (check-agent-record,doc-checkpoint --staged,now-current --staged, trailer and commit-style suites). The other failures it reports —check-release-binary-contract,check-release-workflow,check-test-registrationand their three mutation suites — reproduce identically on a cleanmainand are unrelated to this diff, andtest_cpu_x86_llamacpp_flooris #618 firing at loadavg 195 on the dev box.No lifecycle state changed, so no
STATUS/BENCHMARKSwrite is owed; the row staysSPIKEwith no engine code..agents/issue-index.mdis appended at the end only.🤖 Generated with Claude Code