Skip to content

Upgrade llama.cpp from b10456 to b10679 - #403

Merged
bernardladenthin merged 76 commits into
mainfrom
claude/java-llama-cpp-b10618-hbc7ag
Aug 29, 2026
Merged

Upgrade llama.cpp from b10456 to b10679#403
bernardladenthin merged 76 commits into
mainfrom
claude/java-llama-cpp-b10618-hbc7ag

Conversation

@bernardladenthin

@bernardladenthin bernardladenthin commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Summary

Upgrades the pinned llama.cpp from b10456 to b10679 across the four pin sites
(llama/CMakeLists.txt GIT_TAG, LlamaCppVersion.LLAMA_CPP_VERSION, the README badge, CLAUDE.md),
walked in individually reviewed steps sized by git diff bytes. Upstream split, gated and renamed
several things across that window, so the bump carries the adaptations with it:

  • getMetrics() rebuilt from two upstream tasks. b10408 (server: refactor + correctness fixes for metrics ggml-org/llama.cpp#26920) reduced
    server_task_result_metrics::to_json() to a bare slot array and b10519 (server: refactor sleep handling, allow access /metrics during sleep ggml-org/llama.cpp#27376)
    split the task in two. No signature moved, everything still compiled and linked, and the Java
    payload was quietly the wrong shape for hundreds of builds. server_metrics_to_json() merges
    SERVER_TASK_TYPE_METRICS and SERVER_TASK_TYPE_SLOT_GET back into the shape the Java API has
    always documented, converting upstream microseconds to the milliseconds the payload uses, and
    surfacing counters that existed only inside the Prometheus text.
  • Two new local patches. 0010 casts vocab_type at upstream's GET /models emit site — since
    the common_json switch (common: add json.h abstraction ggml-org/llama.cpp#27511) an unscoped enum binds to the bool overload
    and serialises as true/false. 0011 makes common_peg_until_parser respect leniency on an
    invalid UTF-8 byte the way it already does on an incomplete one: a single malformed byte
    anywhere in a model's output was turning a finished generation into an HTTP 500. Both are
    upstream-submittable, and both were re-checked at b10679 against the pristine upstream file —
    upstream has fixed neither, so both stay.
  • RouterClient API-key support (server : make models endpoints private when authentication is enabled ggml-org/llama.cpp#26347 dropped /models + /v1/models from
    the public set), ModelParameters.setMmprojDevice() (b10541), and repeat-last-n /
    dry-penalty-last-n rejecting -1
    , the sentinel upstream dropped at b10273
    (Samplers: Remove ability to specify "full-context windows" from history-based samplers ggml-org/llama.cpp#26524).
  • The b10644 → b10649 step broke the compile, in exactly the place the priority review table
    warns about. Upstream threaded a new mtmd_helper_init_opt (video-decode settings) through every
    media-ingesting helper, changing mtmd_helper_bitmap_init_from_file, tokenize_input_prompts and
    format_prompt_rerank. Four call sites adapted; each passes mtmd_helper_init_opt_default(), since
    all of them use mctx = nullptr or handle audio. Only a real compile finds this — server-common.h
    is a same-repo header the project #includes directly rather than one reachable through the
    documented dependency graph.
  • Patch applier made idempotent via a stamp file recording the checked-out llama.cpp commit plus
    each patch's SHA-256, so a reconfigure of an existing build directory no longer aborts.

What changes for consumers

Three things here are visible to callers. Each is expanded below, and CHANGELOG.md carries the same
list for the release notes.

  1. Slot state files written by an earlier build are rejected. LLAMA_SESSION_VERSION 9 → 10 and
    LLAMA_STATE_SEQ_VERSION 2 → 3 (b10642). This reaches LlamaModel.saveSlot / restoreSlot and the
    server's /slots/{id}?action=save; existing files have to be regenerated. The in-memory Session
    snapshot/fork feature is unaffected — it never writes a file. Detail: One consumer-visible break.
  2. ModelParameters.setSleepIdleSeconds now throws IllegalArgumentException on 0 and on values
    below -1.
    Upstream rejects both, and emitting them aborted the entire argv parse into a bare
    "Failed to parse model parameters" that named neither the flag nor the reason. -1 (disable) and
    any positive count are unchanged.
  3. Six InferenceParameters methods are deprecated as silent no-opswithTfsZ,
    withPenalizeNl, both withPenaltyPrompt overloads, withUseChatTemplate and withChatTemplate.
    Upstream reads none of those keys from a request body, so they never had an effect; they still
    compile and still do nothing. Three production call sites that used withUseChatTemplate(true) to
    "enable jinja" were removed.

Not a break, but worth knowing: with setSleepIdleSeconds(> 0) every request now waits out an
in-progress idle sleep before the task is built (wake_server()). With idle sleep off — the default
— it is a no-op.

The final step: b10649 → b10679

No project-source change was required, and that is a checked result rather than an assumption.
server-schema.cpp, server-task.cpp, server-common.cpp and all six tools/server/*.h
headers are byte-identical across the range (compared by blob SHA), as are common/chat.h and
tools/mtmd/mtmd-helper.h — so the request-field set, its bounds, the emitted response keys, the
getMetrics()-class of silent contract break and the tts_engine.cpp surface are all provably out
of scope, not merely "checked and unchanged". The in-scope delta is 172 changed lines over 8
files
; the rest of the 159-file range is tools/ui (rebuilt from GIT_TAG by CI) and backends
this project does not build.

Three patch-target files were touched (common/arg.cpp, server-context.cpp, server.cpp) and all
eight patches still apply with zero fuzz. Patch 0007's standing invariant survives because the
new KV-pool-sizing block in llama_server() sits before the extracted route table, not inside it.

Patch 0001 shrank from 37 to 36 files at this step, then grew back to 37 in the audit below.
Upstream rewrote tests/test-save-load-state.cpp's main() to strip a --models DIR option into its
own filtered_argv and call common_params_parse(fargc, filtered_argv.data(), …). By that patch's own
rule — an embedded caller that builds its own argv must call common_params_parse directly — that
site no longer wants the _main() flip, so the hunk was dropped, not refreshed. The patch itself
is still required, verified rather than assumed: common_params_parse in pristine
b10679:common/arg.cpp still carries the #ifdef _WIN32 count-guarded argv = utf8.ptrs.data()
override, and common_params_parse_main appears nowhere in b10679:common/arg.h — upstream has
adopted neither direction proposed in
ggml-org/llama.cpp#26416.

Two additive upstream features arrived, and both are exposed:

Flag Java API Why it was exposed rather than refused
--kv-unified-per-slot (b10662) ModelParameters.setKvUnifiedPerSlot(int) Registered set_examples({LLAMA_EXAMPLE_SERVER}) and jllama.cpp parses with LLAMA_EXAMPLE_SERVER, so it binds. Its cap half is applied in server-context.cpp's new n_ctx_slot(), whose value this binding reads through server_context_meta::slot_n_ctx — it becomes every slot.n_ctx and is the context budget handed to format_prompt_infill.
--tensor-read-lazy (b10653) ModelParameters.setTensorReadLazy(TensorReadLazyMode) + the new args.TensorReadLazyMode enum (OFF/AUTO/ON, mirroring llama_lazy_mode) No set_examples restriction, and common_model_params_to_llama copies lazy_mode into llama_model_params, so it reaches a plain LlamaModel load.

The --kv-unified-per-slot Javadoc is deliberately narrower than upstream's help text. Upstream's
second effect — sizing the shared KV pool to n_parallel * N when no context size is given — lives in
llama_server(), which a ModelParameters-loaded model never enters, so it applies to NativeServer
only. Stating that half unconditionally would have shipped an accurate-for-upstream sentence that is
wrong for the API it documents; the first draft did exactly that and it was caught by reading
server.cpp rather than the help string.

Two more upstream changes needed no adaptation, each for a specific reason:
llama_model_quantize_params gained max_buf_size, but LlamaQuantizer builds its params from
llama_model_quantize_default_params(), so upstream initialises the new field; and the
get_slot_n_ctx()n_ctx_slot() rename is a private member of the server impl that the project
never calls — it reads the value through the unchanged server_context_meta::slot_n_ctx.

New upstream flags across the whole range: seven exposed, two refused

Nine flags were added across b10456 → b10679; none was removed or renamed. The decision was made per
flag, not by default:

Flag Decision
--n-cpu-ffn / -ncffn (b10645) Exposed as ModelParameters.setCpuFfnLayers(int)
--video-fps, --video-timestamp-interval, --video-ffmpeg-dir (b10647) Exposed as setVideoFps / setVideoTimestampInterval / setVideoFfmpegDir
--mmproj-device / -mmdev (b10541) Exposed as setMmprojDevice(String)
--tensor-read-lazy (b10653), --kv-unified-per-slot (b10662) Exposed as setTensorReadLazy / setKvUnifiedPerSlot (see above)
--spec-synth-len, --spec-synth-rates (b10649) Refused — upstream marks both "benchmarking only"

--n-cpu-moe / -ncmoe is not new — it has existed upstream since b6089
(ggml-org/llama.cpp#15077) and b10649 only refactored its lambda onto a shared helper. It had simply
never been exposed here, so setCpuMoeLayers(int) is added alongside setCpuFfnLayers(int). Both are
the companions to setGpuLayers: where that moves whole layers, these move only the weight class that
dominates a model's size, usually fitting a much larger model into the same VRAM at a smaller speed
cost.

The three --video-* flags were initially refused as "inert without a ContentPart video
factory". The follow-up audit showed that reasoning was wrong twice over, and they are now exposed:
server_context::load_model copies them into its own mtmd_helper_init_opt when the projector loads,
and that init_opt is what server-context.cpp hands to process_mtmd_prompt on the task.cli_files
path this binding drives — so they take effect for any media a caller attaches. Video decoding is also
genuinely compiled in: MTMD_VIDEO defaults ON (gated only on LLAMA_SUBPROCESS, also ON) and the
shipped libjllama.so carries the ffmpeg invocation strings. setVideoFfmpegDir is the one that earns
its place — upstream otherwise resolves ffmpeg/ffprobe from PATH, which a JVM process frequently
does not have them on. The content part itself — upstream's input_video, which takes raw base64
rather than a data: URI — remains in TODO.md.

The --spec-synth-* pair synthesises fake per-position acceptance probabilities so llama.cpp's own
speculative harness can be measured without a draft model — an instrument for benchmarking llama.cpp,
not a knob for an application. That refusal stands, with its reasoning in TODO.md.

One consumer-visible break

include/llama.h bumped LLAMA_SESSION_VERSION 9 → 10 and LLAMA_STATE_SEQ_VERSION 2 → 3 (at
b10642, inside the b10639 → b10644 step), following from llama_kv_cell_ext gaining a tok field
that must survive a state save/restore.

This is a state-file format break, not an API break, and it is reachable from public API: the
public LlamaModel.saveSlot(int, String) / restoreSlot(int, String) reach
llama_state_seq_save_file, which is stamped with that constant. Every slot file written by an earlier
build is rejected. Both methods now document the version lock, and that a mismatch surfaces as
upstream's misleading "Unable to restore slot: No available space in KV cache or invalid slot save file". The in-memory Session snapshot/fork feature is unaffected — it never writes a file.

Post-upgrade audits

Four rounds. The first covered b10456→b10644 (completeness, adaptation correctness, test integrity).
The second re-audited what the first could not see: its own fixes, the b10644→b10649 adaptation, and
the CI changes. The third mutation-tested every guard the first two added and swept for untested
surfaces. The fourth re-ran all of it against the b10679 target with five parallel agents.
Each round found mistakes made by the previous one, which is the argument for having run all of
them.

No missed adaptation, confirmed repeatedly. The mechanical server-contract checks are identical at
both ends of every range (b10644→b10649: 68 request fields and 23 bounds, unchanged; b10649→b10679: the
question does not arise, since the emitting files are byte-identical). libjllama.so links with
zero undefined upstream symbols — 531 undefined, all of them versioned libc/libstdc++/libgomp plus
three _ITM_*/__gmon_start__ stubs — so there is no latent macOS-ld64/MSVC link error of the
server_mcp kind. Patch 0007's route-table invariant was re-verified byte-for-byte at b10679: the
extracted helper, the patch's removal block and the pristine upstream lines all hash identically.

Round 1 findings (all fixed)

Finding Fix
patches/0010 had no guardCommonJsonEnumTrap builds its own JSON and calls no project code, so reverting the cast left it green. NativeServerAttachIntegrationTest drives GET /v1/models and asserts vocab_type is an integer — and doubles as the signal to drop the patch if upstream ever casts it themselves.
train_engine.cpp carried the same postprocess_cpu_params pair as tts_params.hpp, inline, and its only test never runs in CI. Both call one shared jllama::resolve_cpu_params (cpu_params.hpp).
testGetMetrics checked 8 of the 21 merged keys, so a rename in the C++ helper alone still shipped. Asserts the full set, with the right JSON type per key.
Two gates still bypassed the resolver whose absence muted the whole model-backed suite. Both routed through it, and both modules gained a rule that fails if a new test reintroduces one.
The langchain4j streaming test had been weakened to "content or thinking", which a content-routing regression passes. Budget raised past Qwen3-0.6B's thinking block so it asserts real content again.
withTfsZ, withPenalizeNl, both withPenaltyPrompt overloads do nothing — those fields appear nowhere upstream and the schema discards unknown fields. Deprecated with an explicit "ignored by the server" note.
getModelMeta() emitted only vision and audio; upstream has tracked has_inp_video for releases. modalities.video emitted, ModelMeta.supportsVideo() added.
The -1 sentinel was cited as b10275 in 4 Javadoc blocks, 4 exception messages and ~9 docs. It is b10273. All corrected.

Round 2 findings (all fixed)

The round-1 fix for idle-sleep was itself wrong, and two of the guards round 1 added did not actually
guard anything — both established by mutation, not by reading.

Finding Fix
Runtime defect. With setSleepIdleSeconds(> 0) the model became permanently unusable after the first idle period. server_queue::post() does not leave the sleeping state — it notifies the condition variable, whose predicate tests req_stop_sleeping, so the loop woke, re-tested and slept again with the task still queued. Upstream wakes for the caller in server_res_generator's constructor, but only for readers from create_response(); this layer uses the CLI-facing get_response_reader(), and nothing here called wait_until_no_sleep() at all. Round 1's is_sleeping() predicate turned the hang into a thrown "No result" — it did not fix the cause. All six post sites go through wake_and_post(). The predicate stays as a backstop, with its comment corrected.
The resolve_cpu_params tests were claimed to guard the trainer. They do not: train_engine.cpp is compiled into jllama only, never jllama_test, and LlamaTrainerIntegrationTest is gated on a property no CI job sets. Deleting the call left the suite green. Assembly extracted to train_params.hpp (sibling of tts_params.hpp). Mutation-verified.
The new source-scanning rule matched only a bare string literal, so it could not see the house idiom (a PROP_* constant) or a wrapped call — it had zero true positives and could never fire. Rewritten to inspect each call's own argument list. Mutation-verified against both forms. Its directory check was assumeTrue — a rule against silent skipping that silently skipped; now assertTrue, plus an empty-scan failure.
The parent-directory resolver test passed with the branch deleted (System.setProperty("user.dir") does not move the process CWD). Dropped in favour of the pre-existing test that does fail; the langchain4j mirror gained the branch test it never had, also mutation-verified.
withUseChatTemplate / withChatTemplate are silent no-ops — upstream reads neither key from a request body — and three production call sites used withUseChatTemplate(true) to "enable jinja for tools", which it cannot do. Both deprecated, the three calls removed. Tool calling was unaffected only because upstream defaults use_jinja to true.
The modality fixtures all set audio and video alike, so a getter wired to the wrong key passed; and nothing asserted the native side emits modalities.video. Discriminating fixtures, plus the first assertion against the real emitter.
Version claims wrong in shipped Javadoc: the /metrics idle-timer exemption is b10519 (ggml-org/llama.cpp#27376), not b10644; the LLAMA_STATE_SEQ_VERSION bump is b10642, not b10644; --n-cpu-moe is b6089, not b10649. All corrected.
The langchain4j integration job is model-backed and crosses JNI but had neither a crash-log echo nor a dump upload. Both added, matching the six test-java-* jobs.

Round 3 findings (all fixed)

A third pass — a 25-agent workflow scanning Java API coverage, C++ helper coverage, diff correctness
and cheap pre-existing gaps, then adversarially verifying each finding — returned 39 findings, 18
upheld. Two were defects in code round 2 had just added, both re-confirmed against pristine
upstream before anything was changed.

Finding Fix
setVideoFps rejected every value <= 0 — but mtmd-helper.h documents fps_target as "desired output fps; <= 0 means use the video's native fps", and the decoder resolves it as fps_target = arg > 0 ? arg : orig_fps. The guard deleted the only way to say "match this clip's own rate": the target is fixed when the projector loads, long before a clip is attached, so a caller cannot supply the rate instead. Rejects only NaN/infinity now — infinity is the value that genuinely breaks, reaching ffmpeg as fps=inf. The same header also showed the test comment claiming interval = 0 meant "a timestamp every frame" was backwards: <= 0 means no timestamps.
setMmprojOffload dropped --mmproj-device unconditionally. Only some combinations actually clash: a named device and --mmproj-offload both resolve to (use_gpu=true, device=named) in either argv order, so clearing there discarded a real multi-GPU pin in order to set a field that already defaults to true. Clearing restricted to the pairs whose meaning depends on argv order. The README paragraph that still claimed the two are mutually exclusive was replaced with a truth table re-derived from the two upstream handlers.
setVideoTimestampInterval accepted values above INT_MAX. The upstream field is int64_t, but the flag is registered with an int handler dispatched through std::stoi, so a larger value throws out_of_range, common_params_parse returns false, and the caller sees only "Failed to parse model parameters" — naming neither the flag nor the reason. Bounded at INT_MAX, with the boundary itself asserted so the guard cannot drift to >=.

Six test groups were added, each checked to fail on the regression it guards: ModelParametersTest
(sentinel passthrough, non-finite rejection, the INT_MAX boundary, both directions of the mmproj
rule), test_tts_params.cpp (TrainParams, mutation-checked), test_json_helpers.cpp
(parse_positive_int_config above/at INT_MAX), test_tts_wav.cpp (the put_u16 fmt fields — no
test read a u16 from that header, so the s390x big-endian gate could not observe a byte-order
regression at all
), TrainingParametersTest (LR-schedule wire keys, whose native fallbacks are
byte-identical to the Java defaults, so a renamed key silently reverts the schedule), and
LangChain4jMappingTest (pins that no "use_jinja" is emitted).

Seven items were deferred to TODO.md rather than widening a version bump — most notably that
ModelParameters emits five CLI flags the server arg parser rejects (--grp-attn-n/-w are
set_examples-scoped away from LLAMA_EXAMPLE_SERVER, so a grep sweep is structurally blind to
them, and four existing tests pin the dead literals happily). That list was re-verified at b10679 and
is still accurate and complete; this branch adds no new dead flag.

Round 4 (b10679) — five agents, complete

Five parallel agents audited the b10679 target: upstream-completeness over the full b10456→b10679
diff, Java API semantics, test integrity by mutation, patches/build/CI, and documentation
fact-checking. Each was given this branch's complete diff, the upstream diff, and a tagged llama.cpp
checkout as the authority — with the explicit instruction that this repo's own docs are under audit,
never evidence.

No blockers. Two agents independently confirmed the adaptation is complete: across the whole
range 9 CLI flags were added, 0 removed, 0 renamed, and all 9 are either exposed or refused with a
written rationale — nothing silently missed. The mechanical contract checks come out identical end to
end, and for the final step the question does not arise at all: the emitting files are byte-identical.

Test honesty, measured rather than asserted. The mutation agent applied 27 mutations and 26 went
red
on the test that claims them. The one that did not is host-dependent by design (a thread-count
fixture that coincides with a 4-core runner), is covered by a sibling test that derives its probe from
the host, and the file's own comment anticipates it. Independently, PIT: 318 generated, 318 killed,
0 survived, 0 no-coverage, test strength 100%
. Rounds 1–3 each found tests that passed with their
subject deleted; this round found none.

Three real defects, two of them introduced by this PR:

Finding Fix
patches/0001 never flipped tools/tokenize/tokenize.cpp. The patch removes the Windows UTF-8 argv recovery from common_params_parse, so every unflipped standalone main() loses it — and llama-tokenize is a real binary passing the process argv. Impact here is zero (LLAMA_BUILD_TOOLS is OFF, so the file never compiles), but the patch is documented as the complete upstream change and submitting it verbatim would have regressed that binary on Windows. Flip added: 36 → 37 files, all 8 patches re-verified against a pristine b10679 checkout with zero fuzz.
The concurrency block added earlier in this PR did not do what its comment claimed. cancel-in-progress: false does not protect a release run: GitHub cancels a pending run whenever a newer run joins the same group, independent of that setting — so a queued publish_to_central dispatch on main could be silently dropped by a later push to main. Every non-PR run now gets its own group via run_id, so it can never be queued behind a sibling. PR runs still share a group per ref and supersede each other — verified empirically: the next push cancelled all 62 jobs of the previous run.
setMmprojDevice("none") cleared --no-mmproj-offload, although that pair resolves to (use_gpu=false, device=null) in either argv order. The code comment listed three of the four combinations while the README table written earlier in this PR listed four — the two disagreed. Exactly one flag is contradicted per device value, never both. The missing fourth-row test is mutation-verified.

Nine wrong upstream tag attributions, seven of which ship in the javadoc jar. Every one named a tag
this bump chunked through rather than the tag that introduced the change; two were written during
this round. Each corrected value was bisected against the tags: --n-cpu-ffn b10645,
the three --video-* flags and mtmd_helper_init_opt b10647, --kv-unified-per-slot b10662,
--tensor-read-lazy b10653, dedup-cache-models b10505, and the server-schema.h
n_ctx_slot drop b10273 — the same commit as the -1 sentinel removal, not the unrelated b10275
break it had been filed under.

Three wrong mechanisms, all in text a future bump would act on — including the justification for
exposing --kv-unified-per-slot, which claimed the cap reaches eval_llama_cmpl_schema and moves the
repeat_last_n / dry_penalty_last_n sentinel expansion. Both false at b10679: that function lost its
n_ctx_slot parameter at b10273, and both fields carry set_hard_limits(0, INT32_MAX), so no sentinel
expands to anything. The conclusion held; the reasoning did not.

One number was removed rather than corrected. An earlier revision of this description claimed "286
response keys identical across the range". Swept four ways, the count comes out 229 / 282 / 254 / 317
depending on the extraction — never 286, and the figure could not be reconstructed. The claim now
states only the part that is both load-bearing and checkable: the key set does not move.

Three coverage gaps, two closed here:

  • wake_and_post had no guard of any kind — the largest new runtime behaviour in jllama.cpp.
    jllama.cpp is not in the jllama_test target, and the only setSleepIdleSeconds reference
    asserted the flag string. Closed with IdleSleepWakeIntegrationTest, and that test then found
    two real JVM crashes
    — see below. It is the most valuable thing this audit produced.
  • jsonSchemaToGrammar needs no model but was tested only inside a model-gated class, so on a
    host without GGUFs the native call was never made. Both directions moved into
    NativeLibraryLoadSmokeTest (now 4 tests, 0 skipped). The LlamaSystemProperties.PREFIX guard hole
    fixed earlier in the core module turned out to exist in the langchain4j copy too; closed there as
    well.
  • patches/0010 still has no guard that runs on a model-free host — reverting its cast leaves
    ctest at a clean 520/520. Its model-gated guard does run on all six CI Java jobs, so this is a
    coverage gap rather than a shipping risk; recorded in TODO.md with the reproduction.

A documentation claim was corrected along the way that matters more than it looks: CLAUDE.md said a
class whose @BeforeAll assumption fails "reported as skipped". It does not — Surefire records
tests="0" errors="0" skipped="0", i.e. nothing at all. Any future guard of the form "did this run
skip anything?" would be structurally blind to it, which is exactly how the model-backed suite stayed
silently muted for months. The durable fix — a floor on tests actually executed — is in TODO.md.

Also recorded rather than folded in, both pre-existing: two all-*-aarch64 fat jars are attached to
releases with no smoke job (violating this repo's own "no release asset that CI has not run" rule,
on runners the workflow already uses), and the patch applier silently accepts a partially-reverted
source tree (CI is unaffected — every job configures into a fresh build directory).

The two crashes the new idle-sleep test found

Adding one model-backed test to an unguarded path immediately produced a deterministic SIGSEGV on
all six CI platforms
. Both defects are pre-existing — reachable from public API for as long as
--sleep-idle-seconds has existed — and neither is caused by the version bump. Neither could be
reproduced locally: this sandbox has no GGUF models, so CI was the only instrument.

The mechanism behind both: upstream's handle_sleeping_state(true) calls destroy(), which frees
the model and context and nulls ctx_tgt/model_tgt; resuming calls load_model() again and
produces a new model. Three things in this layer assumed otherwise.

Crash 1 — llama_context::get_model(), on main. server_context::get_meta() dereferences
ctx_tgt/model_tgt and is read on every request to build the task. Upstream says as much of the
sibling accessor in its own header: "get the underlaying llama_context, can return nullptr if
sleeping"
. Separately, jctx->vocab is captured once after the initial load and used on ~16
tokenize/detokenize/rerank/infill paths — the reload replaces the model, so it dangles. (Its comment
claimed "valid for the lifetime of this context", true only while idle sleep is off, which is the
default and why this was never seen.) The round-2 wake_and_post() fix was necessary but not
sufficient: it wakes at post time and both reads happen before the post. Both now go through one
wake_server() choke point that waits out the sleep and re-reads the vocab.

Crash 2 — load_progress_callback(), on the worker thread, mid-reload — and it was ours. Fixing
crash 1 made the resume actually happen, and the SIGSEGV moved, which is how we knew the first fix
worked. Upstream declares the progress user data as a stack local of load_model() and stores its
address into a field that outlives the call — sound only because they re-assign both fields
unconditionally every load. patches/0002 made that conditional on == nullptr so a caller's
LoadProgressCallback survives; on resume the guard saw our own callback from the first load, skipped the
re-assignment, and left user_data pointing into a dead stack frame. A latent defect in that
patch since the day it was written, reachable only via a second load_model(). The guard now accepts
its own callback too, and CLAUDE.md records why the second disjunct exists — it otherwise reads as
redundant and would be simplified straight back into the crash.

Confirmed by CI, which is the only place it could be: all six Java jobs failed on b32485f, all
six failed differently on 9eb94b7, and all six pass on cce1911.

A smaller sibling defect surfaced on the way: setSleepIdleSeconds emitted 0, which upstream's
handler throws on — aborting the entire argv parse into a bare "Failed to parse model parameters".
A test was pinning that value as correct. Both fixed, with the -1 boundary asserted.

Test plan

  • Local verification at the b10679 pin, from a fresh build directory through the real
    FetchContent path so the fail-loud PATCH_COMMAND ran for real — checked first, because three
    patch-target files changed in the final range. All 8 patches applied with zero fuzz (stamp
    pinned to 50f068ff), Release build clean, ctest 520/520, NativeLibraryLoadSmokeTest 4/4
    with 0 skipped (including nativeBuildInfoMatchesPinnedVersionConstant, the cross-check that
    the four pin sites and the linked build-info agree), full Java suite 1475 run / 0 failures /
    17 model-gated skips
    , llama-langchain4j 46 run / 0 failures, whole-reactor mvn verify
    clean (SpotBugs 0 bugs, enforcer incl. dependency convergence, javadoc jar), clang-format
    22.1.8 clean across the enforced set, publish.yml parses with 62 jobs.
  • Every guard added by this PR mutation-tested. Round 4 applied 27 mutations; 26 went red
    on the test that claims them. The one that did not is host-dependent by design and covered by a
    sibling test that derives its probe from the host. PIT: 318 generated, 318 killed, 0 survived,
    0 no-coverage, test strength 100%
    — the value/args/exception/json gate at threshold 100.
  • CI complete and green on the b10649 head (run 33124949458): 64 jobs — 56 success, 2
    failure, 6 skipped
    . The only two failures were the Verify GPG signing key jobs below; the six
    skips are the publish/release jobs, which correctly do not run on a PR branch.
    • Tests: all six Java Tests jobs (Ubuntu, macOS 14, macOS 15 Metal and no-Metal, Windows
      Ninja and MSVC), Integration Test llama-langchain4j (model-backed), C++ Tests, and
      Build and Test llama-kotlin.
    • Release gating: Package JARs, Package all-backends fat jars,
      Package + Validate Android AARs, and all three artifact smokes —
      Smoke test packaged natives (macOS), Smoke test all-backends fat jar (Linux) and
      (Windows). These are the checks that actually exercise the published artifact, and the macOS
      one exists because a corrupt dylib once shipped in three releases under a fully green pipeline.
    • Builds: every GPU/backend cross-compile, including both CUDA jobs.
  • CI on the b10679 head confirms the idle-sleep fixes (run 33243049437, head cce1911):
    all six Java Tests jobs pass — Ubuntu, macOS 14 Metal, macOS 15 Metal and no-Metal, Windows
    Ninja and MSVC. That matters because it is the only instrument available: the crashing path needs
    a real GGUF, which this sandbox does not have. The progression is the evidence — 6/6 failed on
    b32485f at llama_context::get_model(), 6/6 failed differently on 9eb94b7 at
    load_progress_callback(), 6/6 pass here. Also green: C++ Tests, the spotless + SpotBugs style
    gate, llama-langchain4j, llama-kotlin, vmlens, the WebUI build, verify-model-cache on all
    three OSes, and every completed native build (Linux x86-64/aarch64/s390x, Windows x86-64/x86/arm64,
    both Android, Vulkan ×2, ROCm ×2, CUDA ×2, OpenVINO ×2, SYCL ×3, OpenCL ×2). The run is now
    complete: 64 jobs — 56 success, 2 failure, 6 skipped
    , the same shape as the b10649 run. The two
    failures are the Verify GPG signing key pair below; the six skips are the publish/release jobs,
    which correctly do not run on a PR branch. The release gate came in green after the long
    Build Windows 2025 x86_64 CUDA job (1 h 56 m — nvcc is uncached on Windows by design):
    Package JARs, Package all-backends fat jars, Package + Validate Android AARs,
    Smoke test packaged natives (macOS), Smoke test all-backends fat jar (Linux) and (Windows).
    Coveralls reports 87.011 %; SonarCloud, CodeQL, osv-scanner and clang-format are all green.
  • Red checks, none of them this PR's code:
  • Docs / CHANGELOG updated — CLAUDE.md, CHANGELOG.md, README.md,
    docs/history/llama-cpp-breaking-changes.md (a change-inventory row and a paired verification row
    per range, including b10649→b10679), docs/upgrade/llama-cpp-version-bump.md, TODO.md.

Related issues / PRs

Upstream references are fully qualified (ggml-org/llama.cpp#N); a bare #N anywhere in this
description is a PR in this repository.

patches/0011 is not tied to a version in that list — the invalid-UTF-8 branch is long-standing
upstream behaviour. It surfaced here only because this is the first CI run in which the model-backed
suite actually executed.

Checklist

  • I have read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • My commits follow Conventional Commits — every subject rewritten to the prefixes
    CONTRIBUTING.md lists, each under 72 characters. The rewrite touched messages only: the tree at
    the rewritten head was byte-identical to the pre-rewrite backup, verified with an empty git diff.
  • No security-sensitive changes

https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH

Bernard Ladenthin and others added 30 commits August 25, 2026 08:59
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Upstream #27376 splits server_task_result_metrics: the /slots payload moves
to a new server_task_result_slots fed by SERVER_TASK_TYPE_SLOT_GET. Re-point
handleSlotAction LIST at the new task (it still compiled, but would have
returned an empty object) and move the C++ slots assertions to the new type.
Refresh patch 0007, whose route-table removal block no longer matched after
upstream #26347 dropped two trailing comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Upstream #27511 replaces the server json alias (nlohmann::ordered_json)
with the new common_json pimpl wrapper. Migrate the project C++ to it and
close the two traps that compile silently: a common_json still binds to a
const nlohmann::json parameter through operator std::string() (turning
require_json_field into a runtime type_error inside handleInfill), and an
unscoped enum binds to the bool ctor, so vocab_type was serialised as
true/false. Both are now covered by tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Also refreshes documentation the walk proved stale: the removed -DLLAMA_TAG
second pin, the patch list in the bump runbook, the JSON_ASSERT/json comment
in utils.hpp, and the claim that <cpp-httplib/httplib.h> resolves through a
PUBLIC ../vendor include dir on llama-common (upstream #27304 replaced it
with PUBLIC vendor::nlohmann / vendor::sheredom, which re-export the same
root). Records the pre-existing ServerMetrics payload drift in TODO.md.

Verified at the target: fail-loud PATCH_COMMAND clean on a fresh FetchContent
configure, full build, ctest 491/491, mvn test 1405 run / 0 failures,
NativeLibraryLoadSmokeTest green, clang-format 22.1.8 clean, javadoc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The b10585 trap was that a common_json still binds to a const nlohmann::json
parameter through operator std::string(). The fix so far was a template plus a
warning comment; make it structural by dropping the now-unused
nlohmann/json.hpp include from jni_helpers.hpp, json_helpers.hpp and
jllama.cpp, so that conversion is not reachable from those translation units
at all. test_jni_helpers.cpp keeps testing the helper with a real
nlohmann::json and now includes the header itself. log_helpers.hpp and
train_engine.cpp are unaffected -- they use nlohmann deliberately.

Also corrects a comment: the split-off METRICS to_json() returns json{}, which
is JSON null, not an empty object.

TODO.md records three pre-existing defects found while verifying the bump, none
of them introduced by it: model-gated Java tests silently self-skip in CI
(Surefire resolves models/ against llama/, CI restores it to the repo root --
verified with a throwaway probe); the getMetrics() payload became a bare slot
array at b10408 and ServerMetrics, LlamaModelTest#testGetMetrics and
OpenAiCompatServer /slots all still assume the old object shape; and the patch
applier cannot detect an already-applied 0001 because 0006/0007 rewrite the
same region of server.cpp, so any reconfigure of an existing build dir fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The enum-to-bool trap fixed in jllama.cpp during the b10585 step also hit
upstream own get_res_model_info(), which builds the GET /models and
GET /v1/models payload and emits {"vocab_type", meta.model_vocab_type} --
an unscoped enum. common_json_value integral ctor template is is_integral
gated, which excludes enums, so it binds to the bool ctor and the numeric
vocab type goes on the wire as true/false. It was correct while the alias was
nlohmann::ordered_json, so upstream regressed it silently in #27511.

This ships: server-context.cpp is compiled into libjllama and both routes are
served by NativeServer, the default fat-jar Main-Class, in full and in attach
mode. patches/0009 casts the value to int at the emit site, mirroring what
jllama.cpp already does. A sweep confirmed it is the only such site in the
upstream server sources the project compiles: server_context_meta has exactly
two enum-typed members and the other, pooling_type, is never serialised.

Also corrects two history rows that were scoped too narrowly (the b10585 row
claimed no JSON output shape changed anywhere; the b10507 row dismissed hidden
router models without accounting for NativeServer raw argv passthrough), and
records two RouterClient limitations found by the same review: it cannot send
an API key, and /models stopped being a public endpoint at b10519.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The review caught three documentation defects in the previous commit.

0009 is a burned number: it names the subprocess.h patch dropped at the b10280
bump, and CLAUDE.md carries a prose note three lines below the patches table
saying so. A live 0009 row above it would read as if that note described the
new patch, whose documented precedent would then be "upstream merged it,
delete it". Renamed to 0010 -- filename-only, same sort position, re-checked by
applying the whole set to a clean b10618 checkout -- and the row now explains
why, and states the bump-time rule every neighbouring row carries: if upstream
casts the value themselves, drop this patch rather than refresh it.

The final-target verification row certified a 6-patch build; the shipped tree
has 7. Restated for the re-run: 7 patch markers including the new cast, 491/491
ctest, 1405 Java tests. The bump runbook enumerated six patches by name one
commit after being rewritten -- replaced with glob-truthful wording, since
apply-llama-patches.cmake globs the directory and any list here can only rot.

Also records that --mmproj-device was left unwired on purpose, so the next
reviewer does not re-derive it: it is new public Java API, reachable meanwhile
through NativeServer raw argv passthrough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The docs-accuracy review audited the ~50 new history rows against the upstream
mirror and found the per-range patch-risk prose rests on a directory scan
(common, tools/server, tools/mtmd, vendor, CMakeLists) that misses 35 of the 40
files the patch set touches -- patch 0001 alone flips ~34 standalone main()
call sites under tools, examples and tests. Two ranges did touch one of those
and said otherwise: b10488-b10499 (tools/perplexity/perplexity.cpp, two lines
from an 0001 hunk, the closest call of the walk) and b10590-b10593
(tests/test-recurrent-state-rollback.cpp, +172 lines above an 0001 hunk). Both
rows corrected, "the five patched files" relabelled as the five core files, and
the final row now explains how to read the per-chunk prose and gives the
mechanical check. The verdicts themselves never came from that scan -- each
chunk applied the whole set to a clean checkout, which covers all 40 files.

Also corrected: json{} is JSON null, not an empty object (two docs still said
otherwise after the code comment was fixed, and one cited a test name that no
longer exists); the common_json migration touched fourteen literals across three
files, not seven, and test_utils.cpp direct-init json{1,2,3} is a second,
distinct incompatibility; MTMD_VIDEO stays on for Android here because dockcross
sets neither ANDROID nor CMAKE_SYSTEM_NAME, not because LLAMA_SUBPROCESS is
unset (upstream defaults it off there); git ls-tree compares blob hashes, so the
unchanged-file-set claim needs --name-only.

And two places where this branch shipped a claim it had itself disproved:
CLAUDE.md and the applier header called the patch applier idempotent, and the CI
model policy said the model tests run on every platform. Both now state the real
behaviour and point at TODO.md, as does the vocab_type guard note, since the
LlamaModelTest assertion is model-gated and only CommonJsonEnumTrap runs in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
A reconfigure of an existing build directory always aborted with
"apply-llama-patches: 0001-... does not apply cleanly -- a llama.cpp version
bump probably shifted the patched code", which is not what happened. The
applier decided "already applied?" with a per-patch `git apply --reverse
--check`, and `--check` never mutates the tree: 0006/0007 rewrite the region of
tools/server/server.cpp that 0001 also patches, so reversing 0001 against the
fully patched tree does not match, the applier fell through to a forward apply,
and that failed too. Reversing the scan order does not help for the same reason
-- nothing is ever undone between checks.

The decision now comes from a stamp file in the fetched source tree
(.jllama-patches-applied: the checked-out llama.cpp commit plus each patch's
SHA-256) combined with git's clean/dirty state, which is the part that makes it
safe:

  clean tree -> nothing applied yet (fresh fetch, or a re-checkout after a
                GIT_TAG change), apply forward and write the stamp;
  dirty tree -> already patched: no-op when the stamp matches this exact commit
                and patch set, otherwise abort saying to configure into a fresh
                build directory.

Deriving from git state rather than the stamp alone is what stops the dangerous
case: a stamp left behind after FetchContent re-checks-out a new tag would
otherwise skip patching and silently ship an unpatched build. A source tree
supplied via -DFETCHCONTENT_SOURCE_DIR_LLAMA.CPP that is not a git work tree has
neither oracle and keeps the old per-patch path.

Verified against the real tree (the previously fatal `cmake -B build`
reconfigure now succeeds; a fresh configure applies all 7 patches to a clean
clone, writes the stamp, and reconfigures as a no-op) and against a
two-patches-one-file fixture that reproduces the old failure with the previous
applier and passes with this one, including the stale-stamp-on-a-clean-tree
case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
@bernardladenthin bernardladenthin changed the title Upgrade llama.cpp from b10456 to b10639 Upgrade llama.cpp from b10456 to b10644 Aug 27, 2026
Three independent sweeps over the upstream range -- completeness, adaptation
correctness, test integrity -- found no missed adaptation: the request-field
set, its bounds and the emitted response keys are identical at both ends,
libjllama links with zero undefined upstream symbols, and patch 0007's
route-table invariant still holds byte-for-byte. What they did find were
documentation errors, one real hang, and coverage that did not guard what it
claimed to. All of it is fixed here.

Wrong facts, corrected
- The -1 context-size sentinel was dropped upstream at b10273 (#26524), not
  b10275: git tag --contains puts it in b10273, and b10273..b10275 is empty
  over arg.cpp and server-schema.cpp. It was wrong in 4 shipped Javadoc blocks,
  4 exception messages, and every doc that cited it. b10275 stays where it is
  correct -- the unrelated server-schema.h signature break.
- The state-file break IS reachable from public API. saveSlot/restoreSlot are
  public and reach llama_state_seq_save_file, which is stamped with
  LLAMA_STATE_SEQ_VERSION (2 -> 3 at b10644). The CHANGELOG named the
  package-private handleSlotAction instead, so a consumer grepping for the API
  they call found nothing. Both methods now document the version lock and that
  a mismatch surfaces as upstream's misleading "No available space in KV cache
  or invalid slot save file".
- CLAUDE.md claimed only AudioInputIntegrationTest self-skips.
  LlamaTrainerIntegrationTest does too: net.ladenthin.llama.train.model is set
  by no job and its model is in no models.csv row.

A comment that would have blocked a real fix
  post_and_wait said the idle-sleep guard was unreachable because the getter is
  not on server_context's public header. True premise, wrong conclusion:
  server_response_reader::queue_tasks and server_queue::is_sleeping() are both
  public. Since post_task does not wake a sleeping queue, a getMetrics() issued
  while asleep blocked until close(). The predicate now mirrors upstream's own,
  closing || queue_tasks.is_sleeping(). Needs setSleepIdleSeconds(> 0).

Guards that did not guard
- patches/0010 had none. CommonJsonEnumTrap builds its own JSON and calls no
  project code, so reverting the cast left it green; get_res_model_info is
  static and unreachable from jllama_test. NativeServerAttachIntegrationTest
  now drives GET /v1/models and asserts vocab_type is an integer -- covering
  the patch on every Java platform, and doubling as the signal to DROP it if
  upstream ever casts the value themselves.
- train_engine.cpp carried the same postprocess_cpu_params pair as
  tts_params.hpp -- inline, and its only test never runs in CI. Both now call
  one shared jllama::resolve_cpu_params (cpu_params.hpp) with three tests, so
  the JVM-abort bug cannot regress unseen in the trainer.
- testGetMetrics checked 8 of the 21 merged keys, so a rename in the C++ helper
  alone still shipped and the getter silently returned its default. It now
  asserts the full set, with the right JSON type per key.
- Two gates still read their property with a bare System.getProperty, bypassing
  the resolver whose absence muted the whole model-backed suite. Both routed
  through it, and both modules gained a rule that fails with file:line if a new
  test reintroduces one -- a rule, not a snapshot of today's call sites.
- llama-langchain4j's copied resolver had no test at all; it has one now.
- The langchain4j streaming test had been weakened to "content OR thinking",
  which a content-routing regression passes. Budget raised past Qwen3-0.6B's
  ~200 thinking tokens so it can assert real content again.

Gaps found while looking
- Deprecated withTfsZ, withPenalizeNl and both withPenaltyPrompt overloads:
  tfs_z, penalize_nl and penalty_prompt appear nowhere upstream, and the schema
  discards unknown fields rather than rejecting them, so they have been doing
  nothing while their Javadoc described working knobs.
- setMmprojDevice and setMmprojOffload(false) both write mmproj_use_gpu, and
  argv comes out of a HashMap -- so the winner was hash order. The device
  setter now clears the conflicting flags.
- getModelMeta() emitted only vision and audio while upstream has tracked
  has_inp_video for releases and emits all three from /props; added, with
  ModelMeta.supportsVideo() and tests including the older-metadata shape.
- ServerMetrics gained typed accessors for the window timings
  t_prompt_processing / t_tokens_generation, which were emitted but unreachable.
- getMetrics() documents that the merged payload is not an atomic snapshot and
  that it defers idle-sleep, which upstream's /metrics stopped doing at b10644.

Verified: ctest 512/512 (509 + 3 new), full Java suite 1443 run / 0 failures,
javadoc jar builds clean, spotless applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Two CI-hygiene changes shared with the other three Java repos.

1. "Print crash logs (on failure)" ran on `if: failure()` -- so on EVERY red job,
   the overwhelming majority of which are ordinary assertion failures that never
   write a crash log. It nonetheless printed "The fork died without the JVM
   writing a crash log", stated as fact, under a heading that in this repo also
   echoed perfectly healthy llama-server logs. Reading a normal red test, that is
   an invented crash to chase; it cost real time in this very session. All six
   copies now report the observation, say plainly that no file is the EXPECTED
   case for a normal failure, and name the one signature -- "The forked VM
   terminated without properly saying goodbye", or an exit with no test results --
   that would actually justify the JVM-abort conclusion.

2. `publish.yml` had no `concurrency:` group, so every push started a full
   parallel 64-job pipeline while the superseded ones kept draining; four were
   live at once in one session, which makes "what is CI saying right now"
   genuinely ambiguous and wastes a lot of runner time on results nobody reads.
   `cancel-in-progress` is scoped to `pull_request` ONLY: a push to main or a v*
   tag is a release path and cancelling one midway could leave a partially
   published artifact set, so those always run to completion.

Verified: the workflow parses with the expected concurrency mapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The branch's 60 commit subjects were rewritten to Conventional Commits, which
changes every SHA. Five references in TODO.md name a specific head to tie a
documented CI result to an exact tree ("run 32950691947 on <sha>"), so they
became dangling; they now name the rewritten commits.

The claims stay true: the rewrite touched commit messages only, so each tree is
byte-identical to the one those runs actually validated (verified: `git diff`
between the pre-rewrite backup and the rewritten head is empty).

  f9c43dc -> 999034b   (2 references)
  cbb5e62 -> cfda4a9   (1 reference)
  42ce225 -> 7be24a6   (2 references)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
@bernardladenthin
bernardladenthin force-pushed the claude/java-llama-cpp-b10618-hbc7ag branch from 610d957 to f9f654c Compare August 27, 2026 08:40
claude added 15 commits August 27, 2026 08:43
The `modalities` line gained a third entry (`video`) after the last clang-format
pass in this branch, and I hand-wrapped it -- 22.1.8 fits it on one line. That
turned the pinned `clang-format` check red on every head since the audit commit.

Caught by CI, not locally, because the format pass ran before that edit rather
than after it. Re-verified the whole enforced set afterwards: every file under
src/main/cpp and src/test/cpp now passes `clang-format --dry-run -Werror` with
the pinned 22.1.8 (jllama.h stays excluded -- it is javac-generated).

No behaviour change; ctest still 512/512.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
The first range in this bump that broke the project's own compile, and it broke
it in exactly the place the priority review table warns about.

Upstream threaded a new mtmd_helper_init_opt -- video decode settings: fps
target, ffmpeg binary directory, timestamp interval -- through every helper that
can ingest media. Four call sites lost their signatures:

  tts_engine.cpp:95   mtmd_helper_bitmap_init_from_file   (speaker reference clip)
  jllama.cpp x3       tokenize_input_prompts
  jllama.cpp x1       format_prompt_rerank

Every one of those passes mctx = nullptr or handles audio, so none of them wants
video settings; each now passes upstream's own mtmd_helper_init_opt_default().
Only a real compile finds this: server-common.h is a same-repo header the project
#includes directly rather than one reachable through the documented dependency
graph, which is the second failure class CLAUDE.md's priority table calls out.

The wire contract did not move. All three mechanical greps are identical across
the range -- 68 request fields, 23 bounds, 304 response keys over all six server
.cpp files -- and common/arg.cpp removed or renamed zero flags. common/common.h
and common/speculative.h are additive only.

All 8 patches apply unchanged, which was the first thing verified because six
patch-target files changed in the range (common/arg.cpp, tests/test-arg-parser.cpp,
tools/mtmd/mtmd-cli.cpp, tools/server/server-context.{cpp,h}, tools/tts/tts.cpp).
Upstream's edits fall outside every patched region.

Sizing: 107 KiB reviewable, or 72 KiB excluding ggml-metal -- Metal backend
internals behind unchanged public headers, the same exclusion rationale already
applied to Hexagon -- so a single reviewed step, no chunking.

Of the 7 new upstream flags, two are exposed and five deliberately are not:

  ADDED   --n-cpu-moe / -ncmoe -> ModelParameters.setCpuMoeLayers(int)
          --n-cpu-ffn / -ncffn -> ModelParameters.setCpuFfnLayers(int)
          Real VRAM knobs, and the companions to setGpuLayers: where that moves
          whole layers these move only the weight class that dominates a model's
          size. --n-cpu-moe had never been exposed either. Five tests.

  REFUSED --spec-synth-len / --spec-synth-rates. Upstream's own help text says
          "benchmarking only" -- they synthesise fake per-position acceptance
          probabilities so llama.cpp's speculative harness can be measured
          without a draft model. That is an instrument for benchmarking llama.cpp
          itself, not a knob for an application, and as library API it would
          invite callers to "tune" numbers that fabricate rather than measure.
          Anyone who wants them has them: NativeServer forwards argv verbatim.

  REFUSED --video-fps / --video-timestamp-interval / --video-ffmpeg-dir. They
          configure video decoding and ContentPart has no way to submit a video,
          so they would be inert -- the same dead-but-documented shape that just
          forced withTfsZ / withPenalizeNl / withPenaltyPrompt to be deprecated.
          TODO.md records what a real video-input feature needs, including that
          upstream shells out to ffmpeg/ffprobe.

Verified: 8 patches in the stamp (head 2bb9bddafad4), Release build clean, ctest
512/512, nm -D 40 Java_ exports and 0 mangled, NativeLibraryLoadSmokeTest 3/3
with 0 skipped, full Java suite 1448 run / 0 failures, javadoc jar clean,
clang-format clean across the whole enforced set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
A second verification pass over this branch's own work found one runtime
defect and a set of claims and guards that did not hold up.

The defect: with setSleepIdleSeconds(> 0) the model became permanently
unusable after the first idle period. server_queue::post() does not leave
the sleeping state -- it notifies the condition variable, whose sleeping
predicate tests req_stop_sleeping, so the loop woke, re-tested and slept
again with the task still queued. Upstream performs the wake for the caller
in server_res_generator's constructor, but only for readers built through
create_response(); this layer builds its readers with the CLI-facing
get_response_reader(), which does not, and nothing here called
wait_until_no_sleep() at all. Every later call then blocked until close()
or threw "No result", for the process lifetime. All six post sites now go
through wake_and_post(). The earlier is_sleeping() wait predicate turned
the hang into an exception but did not address the cause; it stays as a
backstop, with its comment corrected to say so.

Extracted the trainer's common_params assembly into train_params.hpp, the
sibling of tts_params.hpp. train_engine.cpp is compiled into jllama only,
never into jllama_test, and LlamaTrainerIntegrationTest is gated on a
property no CI job sets -- so its resolve_cpu_params() call, which guards a
JVM abort, had no runnable guard on any platform. The comment claiming
otherwise was wrong. Verified by mutation: removing the call now reds
TrainParams.ResolvesBothCpuThreadCounts.

Deprecated InferenceParameters.withUseChatTemplate and withChatTemplate.
Neither key is read from a request body upstream -- use_jinja is set only
by --jinja/--no-jinja, and the only "chat_template" string in common/ or
tools/server/ is the one /props emits -- so both were silently no-ops,
including at three call sites that used withUseChatTemplate(true) to
"enable jinja for tools". Those calls are removed; tool calling was
unaffected in practice only because upstream defaults use_jinja to true.

Made the mmproj device/offload clearing symmetric. Clearing in one
direction still lost the HashMap-order race whenever setMmprojOffload was
called second, which is exactly what the one-sided version claimed to fix.

Strengthened guards that mutation testing showed were weak or vacuous: the
source-scanning rule matched only a bare string literal, so it could not
see the house idiom (a PROP_* constant) or a wrapped call, and had no true
positives -- it now inspects each call's own argument list, and is proven
against both forms; the parent-directory resolver test passed with the
branch deleted, so it is dropped in favour of the pre-existing test that
does fail, and the langchain4j mirror gains the branch test it never had;
the batch-inheritance test derived its fixture from the host core count so
it could pass with the bug on a 3-core runner; the modality fixtures all
set audio and video alike, so a getter wired to the wrong key still passed.
Added the first assertion that the native side actually emits
modalities.video, and the first that the mmproj pair clears both ways.

Corrected version attributions that were wrong in shipped Javadoc: the
/metrics idle-timer exemption is b10519 (#27376), not b10644; the
LLAMA_STATE_SEQ_VERSION bump is b10642, not b10644; --n-cpu-moe has existed
since b6089 (#15077) and was merely never exposed here, only --n-cpu-ffn is
new at b10649. The restore-slot Javadoc quoted an unwrapped message a
caller never sees. The bump record overstated the response-key count and
the diff size and listed a WebUI change the range does not contain.

516/516 C++ tests, 1449 Java tests with 0 failures, javadoc and
clang-format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
setVideoFps, setVideoTimestampInterval and setVideoFfmpegDir map upstream's
--video-fps / --video-timestamp-interval / --video-ffmpeg-dir.

These were refused earlier in this branch on the reasoning that they would
be inert without a ContentPart video factory. That was wrong twice over.

They are not inert. server_context::load_model copies params_base.video_*
into its own mtmd_helper_init_opt when the projector loads, and that
init_opt is what server-context.cpp passes to process_mtmd_prompt on the
task.cli_files path this binding drives -- so they apply to any media a
caller attaches, today. And video decoding is genuinely in the shipped
artifact: MTMD_VIDEO defaults ON, gated only on LLAMA_SUBPROCESS which is
also ON, and libjllama.so carries the ffmpeg invocation strings.

setVideoFfmpegDir is the one that earns its place: upstream otherwise
resolves ffmpeg and ffprobe from PATH, and a JVM process -- an application
server, an Android app, a container built for the JAR alone -- frequently
has neither on PATH even when they are installed. Without this flag there
is no way to point at them.

Raw video bytes already reach the decoder through
ContentPart.imageBytes(bytes, "video/mp4"), because
mtmd_helper_bitmap_init_from_buf sniffs the container and falls through to
video rather than trusting the declared MIME type. What remains is only the
ergonomic entry point, ContentPart.videoFile(Path), which stays in TODO.md
along with the corrected reasoning. The --spec-synth-* refusal stands:
upstream marks those benchmarking-only.

1452 Java tests, 0 failures; javadoc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
It is model-backed and crosses JNI, so a forked test JVM there can abort
exactly the way the six test-java-* jobs can -- but it had neither the
echo step nor the dump upload, so such an abort would have left no
diagnostic on either path. Same step and same path set as those jobs,
scoped to this module.

Pre-existing, not a regression from this branch; noticed while auditing
this session's own CI changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Both tests capped generation at 320 tokens, which is exactly on the
boundary for this model. Run 33109360197 proved it: from the same prompt
against the same Qwen3-0.6B, chatReturnsAssistantText finished thinking at
267 tokens and passed, while streamingDeliversTokensThenCompletes consumed
all 320 inside <think> -- "eval time = 6490.39 ms / 320 tokens" -- emitted
no assistant content, and failed its non-empty assertion.

The assertions are right and stay: a reply that is only a thinking block is
a real failure, and accepting "content or thinking" is what the earlier
8-token version of the streaming test did wrong. The budget was the
problem. Both now use a shared MAX_OUTPUT_TOKENS of 1500, matching the core
module's ReasoningBudgetTest N_PREDICT for this same model, with the
measurement that motivated it recorded on the constant.

It is a cap, not a target -- a healthy run stops around 270-340 tokens --
so this costs nothing except in the pathological case it exists to absorb.
The streaming test's future timeout goes 60s -> 180s, since 60s was sized
for the old cap and leaves no margin for a worst-case 1500 on a shared
runner.

46 tests, 0 failures, 4 skipped locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Both were still marked [OPEN] while their fixes shipped in this same
branch, so the file claimed two live bugs that no longer exist.

SessionForkRewindIntegrationTest: the entry's "not a bump regression"
conclusion was right, but its diagnosis -- "it is the KV restore, not the
bookkeeping" -- was wrong. Qwen3-0.6B is a reasoning model and the tests'
budget was consumed inside <think>, so generation completed normally and
simply returned no assistant content. Fixed in cc67ea7; the identical root
cause resurfaced in llama-langchain4j and was fixed the same way in
dd07b0e.

NativeServerAttachIntegrationTest.completion_overHttp_served: fixed by
local patch 0011 (ca60947) -- common_peg_until_parser honoured leniency on
an incomplete trailing UTF-8 sequence but not on an invalid byte, so one
malformed byte turned a finished generation into a 500. The entry now
records the mechanism and the runnable guard.

No [OPEN] entries remain in the jllama-specific section. The cross-cutting
section is unchanged: those are standing repo-hygiene items, not upgrade
work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Final sweep over this branch's own documentation.

Corrected in CHANGELOG: "304 response keys" -> 286 over the six compiled
server TUs, and "of the 7 new upstream flags" -> 6, with --n-cpu-moe named
as pre-existing (b6089) rather than new. The b10644-b10649 verification row
still said ctest 512/512 and ModelParametersTest 73/73, which the second
audit's TrainParams and video-flag cases moved to 516/516 and 77/77.

Withdrew an untested workaround. An earlier draft told readers to submit
video via ContentPart.imageBytes(bytes, "video/mp4"), reasoning that
mtmd_helper_bitmap_init_from_buf sniffs the container. Reading upstream
properly: the wire type for video is input_video, and it calls
handle_media with accept_base64_uri = false -- raw base64 only, NOT the
data: URI form the image factories build. The image_url route might still
work by sniffing, but it is untested here and additionally gated on
allow_image, so it is no longer documented as supported. README and TODO
now state the verified shape a future ContentPart.videoFile must emit.

README also gained the mmproj device/offload exclusivity rule, which was
behaviour a caller can hit with no note anywhere, and a short video section
covering the three new setters.

Recorded the teardown ordering wake_and_post depends on: upstream's loop
leaves `sleeping` set when it exits on !running, so a caller parked in
wait_until_no_sleep() is released only because Java_..._delete drains
users == 0 before calling terminate(). Verified correct today; the comment
exists so a future reorder does not silently deadlock.

Whole-reactor mvn verify green (SpotBugs 0 bugs, enforcer incl. dependency
convergence, javadoc), ctest 516/516, clang-format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
A workflow of 25 agents re-audited this branch: 39 findings, 18 upheld
after adversarial verification. Two were real defects in code this PR
itself added, both confirmed here against pristine upstream b10649.

setVideoFps rejected every value <= 0, but mtmd-helper.h documents
fps_target as "<= 0 means use the video's native fps" and the decoder
resolves it as `fps_target = arg > 0 ? arg : orig_fps`. The guard deleted
the only way to say "match this clip's own rate" -- the target is fixed
when the projector loads, long before a clip is attached, so a caller
cannot type the rate in instead. It now rejects only NaN and infinity;
infinity is the value that genuinely breaks, reaching ffmpeg as "fps=inf".
The same header shows `timestamp_interval_ms <= 0 means NO timestamp`, so
the test comment claiming 0 meant "every frame" was backwards.

setMmprojOffload dropped --mmproj-device unconditionally. Only some
combinations actually clash: a named device and --mmproj-offload both
resolve to (use_gpu=true, device=named) in either argv order, so clearing
there threw away a real multi-GPU pin to set a field that already defaults
to true. Clearing is now restricted to the pairs whose meaning depends on
argv order -- any device against --no-mmproj-offload, and "none" (which
sets use_gpu=false) against --mmproj-offload.

Also bounded setVideoTimestampInterval at INT_MAX: the upstream field is
int64_t but the flag is registered with an int handler dispatched through
std::stoi, so a larger value aborts the whole argv parse and surfaces only
as "Failed to parse model parameters", naming neither flag nor reason.

Tests, each verified to fail on the regression it guards:
- ModelParametersTest: the two tests that asserted the defects are replaced
  by ones asserting the sentinel passes through, plus non-finite rejection,
  the INT_MAX boundary, and both directions of the mmproj rule.
- test_tts_params.cpp: TrainParams.MapsTheContextAndScheduleFields covers
  the seven build_train_params fields no other test reads. Mutation-checked
  -- deleting the decay_epochs assignment reds it, and nothing else.
- test_json_helpers.cpp: the fractional, above-INT_MAX and exactly-INT_MAX
  terms of parse_positive_int_config, previously covered only for raw <= 0.
- test_tts_wav.cpp: the four put_u16 fmt fields and byte_rate. No test read
  a u16 from the header, so the s390x big-endian gate could not see a
  put_u16 byte-order regression at all.
- TrainingParametersTest: the LR-schedule wire keys, whose native fallbacks
  are byte-identical to the Java defaults -- a renamed key silently reverts
  the schedule instead of failing.
- LangChain4jMappingTest: pins that no "use_jinja" is emitted, the one
  remaining unguarded site of the three no-op calls this PR removed.

Deferred to TODO.md rather than widening the bump: five ModelParameters
flags the server arg parser rejects (--grp-attn-* are example-scoped away
from SERVER, so a grep sweep cannot see them), the context refcount
helpers, OSInfo's archMapping aliases, the trainer's untested end-to-end
path, LlamaLoader's jar internals, and dead Java8CompatibilityHelper code.

Reactor verify green (SpotBugs 0, enforcer, javadoc), ctest 520/520,
1456 Java + 46 langchain4j tests, clang-format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
No project-source change was required, and that is a checked result: every
tools/server/*.h header plus server-schema.cpp, server-task.cpp,
server-common.cpp, common/chat.h and mtmd-helper.h are byte-identical across
the range (compared by blob SHA), so the request-field set, its bounds, the
emitted response keys and the mtmd surface cannot have moved. The in-scope
delta is 172 lines over 8 files; the rest of the 159-file range is tools/ui
(rebuilt from GIT_TAG by CI) and backends this project does not build.

Two additive upstream features arrive and both are exposed:

- setKvUnifiedPerSlot(int) for --kv-unified-per-slot. It is registered
  set_examples({LLAMA_EXAMPLE_SERVER}) and jllama.cpp parses with that
  example, so it binds; its cap half is applied in server-context.cpp's new
  n_ctx_slot(), whose value this binding reads through
  server_context_meta::slot_n_ctx and feeds to eval_llama_cmpl_schema, so it
  also moves where the repeat_last_n / dry_penalty_last_n sentinels expand
  to. The Javadoc is deliberately narrower than upstream's help text:
  upstream's second effect, sizing the shared KV pool to n_parallel * N when
  no context size is given, lives in llama_server() and therefore applies to
  NativeServer only, never to a model loaded from this builder.

- setTensorReadLazy(TensorReadLazyMode) for --tensor-read-lazy, over a new
  args enum mirroring llama_lazy_mode (off/auto/on = 0/1/2). It has no
  set_examples restriction and common_model_params_to_llama copies lazy_mode
  into llama_model_params, so it reaches a plain LlamaModel load.

Two further upstream changes need no adaptation:
llama_model_quantize_params gained max_buf_size, but LlamaQuantizer builds
its params from llama_model_quantize_default_params(); and the
get_slot_n_ctx() -> n_ctx_slot() rename is a private member the project
never calls.

Patch 0001 shrinks from 37 to 36 files. Upstream rewrote
tests/test-save-load-state.cpp's main() to strip --models DIR into its own
filtered_argv before calling common_params_parse, so by that patch's own
rule the site no longer wants the _main() flip; the hunk is dropped, not
refreshed. The patch itself is still required, verified rather than assumed:
common_params_parse in pristine b10679 still carries the count-guarded
GetCommandLineW override and common_params_parse_main is absent from
b10679:common/arg.h. Patches 0010 and 0011 were re-checked the same way and
both remain unfixed upstream. All 8 apply with zero fuzz despite three
patch-target files changing; patch 0007's route-table invariant survives
because the new KV-pool-sizing block sits before the extracted table.

Also corrects a stale README claim that setMmprojDevice and setMmprojOffload
are mutually exclusive with each always clearing the other. Round 3 narrowed
that; the paragraph is replaced by the four-row truth table re-derived from
the two upstream handlers, which shows only two of the four combinations are
order-dependent.

Verified locally at the b10679 pin: fresh build directory through the real
FetchContent path so the fail-loud PATCH_COMMAND ran, Release build clean,
ctest 520/520, mvn clean test 1474 run / 0 failures with
NativeLibraryLoadSmokeTest 3/3 and 0 skipped (the pin cross-check against
the linked build-info), reactor mvn verify BUILD SUCCESS including SpotBugs
and the javadoc jar, clang-format 22.1.8 clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Acts on a five-agent audit of the b10456 -> b10679 bump (upstream API
completeness, Java API semantics, test integrity by mutation, patches/build/CI,
and documentation fact-checking). No blockers were found; the adaptation itself
is complete -- 9 flags added across the range, 0 removed, 0 renamed, all 9 either
exposed or refused with a written rationale; 68 request fields, 23 bounds and the
emitted response-key set identical end to end; all patches applying with zero
fuzz. What follows is what the audit did find.

Three defects, two of them in code changed by this PR:

- patches/0001 never flipped tools/tokenize/tokenize.cpp. The patch REMOVES the
  Windows UTF-8 argv recovery from common_params_parse, so every unflipped
  standalone main() loses it -- and llama-tokenize is a real binary that passes
  the process argv. Impact here is zero (LLAMA_BUILD_TOOLS is OFF, so the file is
  never compiled), but the patch is documented as the complete upstream change,
  and submitting it verbatim would have regressed llama-tokenize on Windows.
  Flip added; 36 -> 37 files; all 8 patches re-verified against a pristine
  b10679 checkout.

- The concurrency block added earlier in this PR did not protect a release run,
  and its comment claimed it did. cancel-in-progress: false does not help:
  GitHub cancels a *pending* run whenever a newer run joins the same group,
  independent of that setting, so a queued publish_to_central dispatch on main
  could be dropped by a later push to main. Every non-PR run now gets its own
  group via run_id and can never be queued behind a sibling. The three sibling
  repos carry the same block and still need this.

- setMmprojDevice("none") cleared --no-mmproj-offload although that pair
  resolves to (use_gpu=false, device=null) in either argv order. The code's
  comment table listed three of the four combinations and the README table
  written earlier in this PR listed four, so the two disagreed. Now exactly one
  flag is contradicted per device value, never both; the missing fourth-row test
  is mutation-verified.

Nine wrong upstream tag attributions, seven of which ship in the javadoc jar.
Every one named a tag this bump chunked through rather than the tag that
introduced the change; each corrected value was bisected against the tags:
--n-cpu-ffn b10645 (was b10649), the three --video-* flags and
mtmd_helper_init_opt b10647 (was b10649), --kv-unified-per-slot b10662 (was
b10679), --tensor-read-lazy b10653 (was b10679), dedup-cache-models b10505 (was
b10507), the /metrics idle-timer exemption b10519 (was b10644, and it
contradicted our own javadoc), and the server-schema.h n_ctx_slot drop b10273 --
the same commit as the -1 sentinel removal, not the unrelated b10275 break it
was documented as. mtmd's video path itself predates the whole bump (b9562);
b10647 only added the CLI flags that surface it.

Three wrong mechanisms, all in text a future bump would act on:

- The justification for exposing --kv-unified-per-slot said the cap reaches
  eval_llama_cmpl_schema and moves where the repeat_last_n / dry_penalty_last_n
  sentinels expand to. Both are false at b10679: that function lost its
  n_ctx_slot parameter at b10273 and takes four arguments, and both fields carry
  set_hard_limits(0, INT32_MAX) so no sentinel expands to anything. The cap does
  reach this binding -- as every slot.n_ctx and as the budget passed to
  format_prompt_infill. Conclusion unchanged, reasoning corrected.
- The patch-0011 note said upstream has no FAIL-on-invalid-UTF-8 test through
  the until parser. It has three; they parse in strict mode, which is the actual
  reason the lenient-only change leaves them green.
- The json_helpers comment said n_prompt_cached has no Prometheus counterpart.
  to_metrics() has emitted llamacpp:prompt_tokens_cached_total since the same
  commit; what it lacked was a JSON representation.

A guard added earlier in this PR had a hole, now closed and demonstrated both
ways: TestConstantsTest's raw-property scan matched only a literal or a PROP_*
constant, so System.getProperty(LlamaSystemProperties.PREFIX + ".tts.model")
passed silently -- a guard against silent test-skipping that skipped silently.
The widened clause fails on that form; the old guard passes 8/8 on the identical
source.

Also: propsJson now emits modalities.video, so it stops contradicting the
ModelMeta.supportsVideo() this PR added; setVideoTimestampInterval documents
upstream's 0 = no timestamps; setCpuMoeLayers no longer claims 0 keeps experts
on the GPU (it adds no override at all); CLAUDE.md's C++ test recipe no longer
shows a five-argument eval_llama_cmpl_schema call removed at b10273; and stale
counts are corrected (twelve tools/server/*.h headers not six, 1474 tests not
1456, three build_tts_params tests not five, 1909 lines not ~1850). The
"286 response keys" figure is removed rather than adjusted: it could not be
reproduced by any sweep, so the claim now states only the part that is both
load-bearing and checkable -- that the key set does not move.

Two pre-existing gaps are recorded in TODO.md with reproductions rather than
folded in: the two all-*-aarch64 fat jars attached to releases with no smoke job,
and the patch applier silently accepting a partially-reverted source tree.

Verified: fresh build directory through the real FetchContent PATCH_COMMAND
(stamp head 50f068ff, all 8 patches), ctest 520/520, mvn test 1474 run /
0 failures / 17 model-gated skips, reactor mvn verify clean, clang-format 22.1.8
clean, publish.yml parses with 62 jobs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Changing the concurrency group expression has a one-shot side effect worth
recording, because it looks like the change is broken when it is not: GitHub
reads `concurrency` from the workflow file at each run's own ref, so a run
started before the change sits in the old group and a run started after it sits
in the new one. Different groups do not supersede, so the push that lands the
change leaves the previous run draining.

Observed here: the run on 267d975 stayed in_progress across the push of
6566b5d instead of being cancelled. It self-heals from the next push onward.
The three sibling repos carry the same block and will see the identical overlap
when the fix is ported to them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Acts on the mutation-testing arm of the b10679 audit. Its verdict on the
existing tests was clean: 27 mutations applied across C++, Java and the fetched
llama.cpp source, 26 red on the test that claims them. The one that stayed green
is host-dependent by design (TtsParams' inherit assertions coincide on a 4-core
host; ResolveCpuParams derives its probe from the host precisely to avoid that,
and went red). PIT is 318/318 killed, 0 no-coverage, test strength 100%, so the
mutationThreshold 100 gate holds with this branch's additions. Earlier rounds
each found a test that passed with its subject deleted; this round found none.

What it did find is production code with no runnable guard. Two are closed here:

- wake_and_post() had zero coverage of any kind. It is the largest new runtime
  behaviour in jllama.cpp and it fixes a defect that made a model permanently
  unusable after its first idle period, yet jllama.cpp is not in the jllama_test
  target and the only setSleepIdleSeconds reference asserted the flag string.
  IdleSleepWakeIntegrationTest loads with a one-second idle window, waits past
  it, and requires three further completions plus getMetrics() to be serviced.
  The bounded @timeout is the assertion that matters: without the fix the method
  does not fail, it hangs.

- jsonSchemaToGrammar needs no model, but both its assertions lived in
  LlamaModelTest, whose @BeforeAll gates the whole class on a GGUF -- so on a
  host without models the native call was never made at all. Moved into
  NativeLibraryLoadSmokeTest, which runs wherever libjllama exists (now 4 tests,
  0 skipped). Both directions are kept: a valid schema must cross JNI and return
  a grammar, and a malformed one must surface as LlamaException rather than
  letting a C++ json::parse exception escape and abort the JVM.

The LlamaSystemProperties.PREFIX hole closed in the core guard one commit ago
existed in llama-langchain4j's copy of the same rule as well; closed there too.

Two documentation corrections, both established by running the thing:

- CLAUDE.md said a model-gated class "reported as skipped". It does not. A
  class-level @BeforeAll assumption makes Surefire record
  tests="0" errors="0" skipped="0" -- the class contributes no entries at all,
  so a guard of the form "did the run skip anything?" is structurally blind to
  it. That is exactly how the model-gated suite stayed silently muted for
  months. Confirmed first-hand: the new model-gated test above reports
  tests=0, skipped=0 on a host with no models.
- The PIT figure was stale (304 -> 318 mutations, all killed).

Three gaps are recorded in TODO.md with reproductions rather than folded in:
patches/0010 has no guard that runs on a model-free host (reverting its cast
leaves ctest at a clean 520/520; the model-gated guard does run on all six CI
Java jobs, so this is coverage rather than shipping risk),
theShippedModelConstantsGoThroughTheResolver is vacuous when the fixture is
absent, and nothing asserts a floor on the number of tests actually executed --
the one check that would have caught the original silent mute directly.

Verified: mvn test 1475 run / 0 failures / 17 model-gated skips, langchain4j
46 run / 0 failures, reactor mvn verify BUILD SUCCESS, clang-format 22.1.8
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
IdleSleepWakeIntegrationTest, added one commit earlier to close the
wake_and_post coverage gap, immediately found a real JVM crash: SIGSEGV at
llama_context::get_model(), deterministic on all six CI platforms, on the first
request issued after an idle-sleep window.

Upstream's handle_sleeping_state(true) calls destroy(), which frees the model
and context and sets ctx_tgt/model_tgt to nullptr; resuming calls load_model()
again and produces a NEW model. Two things in this layer assumed otherwise, and
both are reachable from public API whenever --sleep-idle-seconds is set:

1. server_context::get_meta() dereferences ctx_tgt/model_tgt and is read on
   every request to build the task. Upstream says as much of the sibling
   accessor in its own header -- "get the underlaying llama_context, can return
   nullptr if sleeping" -- so calling it while asleep is a null dereference.
   That is the observed crash.
2. jctx->vocab is captured once after the initial load and used on every
   tokenize/detokenize/rerank/infill path. The reload replaces the model, so the
   cached pointer dangles: a use-after-free that happens to survive only by
   allocator luck. The field's comment claimed it was "valid for the lifetime of
   this context", which holds only while idle sleep is off -- the default, which
   is why this went unseen.

The earlier wake_and_post() fix was necessary but not sufficient: it wakes at
POST time, and both reads above happen before the post. Upstream sidesteps the
whole question by caching meta once after load (server_routes::update_meta) and
serving cached /props and /models while asleep; this layer reads live state per
request, so it must wake first and then re-sync.

Both now go through one wake_server() choke point that waits out the sleep and
re-reads the vocab, plus wake_and_get_meta() for the eight per-request meta
reads. It is called from every entry point that touches the model, and is a
no-op when sleeping is off and in vocab-only mode (which owns its model and has
no server). The load-path get_meta() is deliberately left direct, with a comment
saying why: the model is freshly loaded there and the worker loop is not yet
servicing waits. Teardown must not call it -- a caller parked in
wait_until_no_sleep() when the queue terminates is never released, which is safe
only because every caller runs inside a jllama_context_guard and delete() drains
users to zero before terminate().

Also fixes the same class of defect the investigation surfaced in the builder:
setSleepIdleSeconds emitted 0 and values below -1, which upstream's own handler
throws on, aborting the entire argv parse and reaching the caller only as
"Failed to parse model parameters" -- naming neither the flag nor the reason.
They are rejected here now, with the -1 boundary asserted so the guard cannot
drift. The test that pinned 0 as serialising to "0" was pinning a value the
server cannot accept, and is replaced. Its Javadoc also said the server "shuts
down" after the idle window; it does not, it releases the model and reloads it
on the next request.

Verified: ctest 520/520, mvn test 1476 run / 0 failures / 17 model-gated skips,
reactor mvn verify BUILD SUCCESS, clang-format 22.1.8 clean. The runtime
behaviour itself can only be verified by CI -- this sandbox has no GGUF models,
so the crashing path cannot be reproduced locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
Second crash on the idle-sleep path, found by the same test after the previous
commit fixed the first one. The wake now works -- and the SIGSEGV moved from
llama_context::get_model() on "main" to
server_context_impl::load_progress_callback() on the server worker thread, with
the model reload visibly in progress in the stack. That is a latent defect in
our own patches/0002, reachable only via a SECOND load_model() call.

Upstream declares the progress user data as a LOCAL of load_model():

    load_progress_data load_progress_text(this, "text_model");
    ...
    params_base.load_progress_callback_user_data = &load_progress_text;

storing the address of a stack local into a field that outlives the call. That
is sound upstream only because they re-assign BOTH fields unconditionally on
every load_model(), so the pointer always names the current frame.

patches/0002 made that assignment conditional on `== nullptr` so an embedding
caller's LoadProgressCallback trampoline survives. On the first load the caller's
field is null and ours is installed, pointing at load #1's frame. Resuming from
the sleeping state calls load_model() again; by then params_base holds OUR
callback, the nullptr guard is false, neither field is re-assigned, and
user_data still points into load #1's dead stack frame. The callback dereferences
it on the next load and segfaults.

The guard now accepts its own callback as well as nullptr, so our user_data is
refreshed on every load while a caller-supplied callback is still never
clobbered. CLAUDE.md's patch table records why the second disjunct exists, since
a future reader would otherwise read it as redundant with the nullptr check and
simplify it straight back into this crash.

Verified: all 8 patches apply to a pristine b10679 checkout with zero fuzz;
fresh build directory through the real PATCH_COMMAND (stamp 50f068ff), Release
build clean, ctest 520/520, mvn test 1476 run / 0 failures / 17 model-gated
skips. As with the previous commit the runtime path itself is only reachable in
CI -- this sandbox has no GGUF models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH
@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown
Owner Author

Final CI result — run 33243049437, head cce1911

64 jobs — 56 success, 2 failure, 6 skipped. Identical shape to the b10649 run. The two failures
and the six skips are both accounted for below; nothing red is this PR's code.

The six Java jobs are the whole point of this run

All six pass: Ubuntu x86-64, macOS 14 arm64 (Metal), macOS 15 arm64 (Metal), macOS 15 arm64 (no
Metal), Windows x86-64 (Ninja) and Windows x86-64 (MSVC classifier).

They are the only instrument that could confirm the two idle-sleep crash fixes — the crashing path
needs a real GGUF, and the sandbox this branch was developed in has none. The evidence is the
progression, not a single green run:

Head Result Crash frame
b32485f 6/6 fail llama_context::get_model(), on main
9eb94b7 6/6 fail, differently server_context_impl::load_progress_callback(), on the server worker
cce1911 6/6 pass

The frame moving after the first fix is what showed the fix worked rather than merely relocating the
symptom.

Release gating — all green

The long pole was Build Windows 2025 x86_64 CUDA at 1 h 56 m (nvcc is deliberately not wrapped
by sccache on Windows, so the device passes are uncached). Everything downstream of it then came in
green:

  • Package JARs, Package all-backends fat jars, Package + Validate Android AARs
  • Smoke test packaged natives (macOS), Smoke test all-backends fat jar (Linux), (Windows)
    the three checks that exercise the published artifact rather than a job-local build
  • Android emulator on-device test (x86_64), Integration Test llama-langchain4j (model-backed),
    C++ Tests, Build and Test llama-kotlin, vmlens
  • every native build: Linux x86-64/aarch64/s390x, Windows x86-64/x86/arm64, both Android,
    Vulkan ×2, ROCm ×2, CUDA ×2, OpenVINO ×2, SYCL ×3, OpenCL ×2

Also green outside the workflow: SonarCloud, CodeQL, osv-scanner, clang-format, and Coveralls at
87.011 %.

The three red checks, and why none of them is this branch

  • Verify GPG signing key ×2 — both fail in ~2 s, before any step runs. They declare
    environment: maven-central, whose deployment-branch policy admits only main and tags, so a
    feature branch is rejected at job start. Not re-runnable to a different outcome from here.
  • claude-review — fails API-side: the log shows the SDK returning is_error: true after
    243 ms with num_turns: 1, total_cost_usd: 0 and an empty modelUsage, i.e. the model call
    never happened. No fix for this exists in this repository.
  • License Compliance (a commit status from a GitHub App, not a workflow job) — error,
    17 issues found. Pre-existing: the same finding count is recorded against PRs Add Windows Ninja Multi-Config build with sccache caching #248 and Feature wave: Android AAR + Kotlin facade, server attach/router modes, LangChain4j streaming, GGUF tooling (llama.cpp b9878) #298 in
    TODO.md, and the components it flags are third-party test-scope dependencies (JMH, Logback,
    jcstress, Lincheck, jqwik, JUnit Jupiter). This is what holds mergeable_state at blocked
    not CI. The scanner's host is outside this environment's egress allowlist, so the individual
    findings could not be read from here.

The six skipped jobs are Check: main branch / SNAPSHOT, Check: v* tag, Publish Snapshot to Central, Publish Release to Central, Update Snapshot Pre-release on GitHub and Attach Signed Binaries to GitHub Release — the publish path, which correctly does not run on a PR branch.

One note on the concurrency change in this PR

Run 33215659259, on the now-superseded head 267d975, was never cancelled. That is expected and
one-time: GitHub reads the concurrency group from each run's own ref, and that run predates the
group-expression change. It is self-healing — the very next push did cancel all 62 jobs of run
33217708855. Cancelling the stale run returns 403 from this session, so its status is not a
statement about this PR.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants