diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf25edad..254739af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,43 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
## [Unreleased]
+### Changed
+- **BREAKING (runtime): the shipped SLF4J binding is now `slf4j-simple`, not `logback-classic`.**
+ Two independent reasons, and the first is a hard failure rather than a preference:
+
+ 1. **This artifact targets Java 8 and logback no longer does.** Every logback release from 1.4.0 on
+ is class-file major 55 (Java 11). SLF4J's `ServiceLoader` loads `LogbackServiceProvider` at JVM
+ startup, so a Java 8 consumer got `UnsupportedClassVersionError` before a single line of library
+ code ran. Measured: all 181 classes of logback-classic 1.6.3 are major 55.
+ 2. **The Java 8 logback line is end-of-life with unfixed CVEs.** 1.3.16 (2025-10-29) is its last
+ release; CVE-2026-1225, CVE-2026-9828 and CVE-2026-10532 were fixed only in 1.5.x, and
+ CVE-2026-19880 only in 1.6.3 — which is Java 11 bytecode and therefore unreachable from here.
+ Downgrading would have traded a crash for permanent unpatchability.
+
+ `slf4j-simple` is six classes from the same release train as `slf4j-api`, with no configuration
+ parser, socket server or deserialization — the subsystems essentially every logback CVE lives in.
+
+ **What changes for you:** `logback.xml` is no longer read. Configure with a classpath
+ `simplelogger.properties` or `-Dorg.slf4j.simpleLogger.*`. With no configuration at all, output is
+ quieter than before (logback defaulted the root logger to DEBUG; slf4j-simple defaults to INFO).
+ To keep logback, exclude `org.slf4j:slf4j-simple` and declare your own binding — which is what the
+ SLF4J api/binding split is for.
+
+ **The runnable fat jar carries logging defaults; the library jar deliberately does not.**
+ `simplelogger.properties` (INFO, stdout, timestamps, thread + short logger name — what the previous
+ logback default emitted) is added by the assembly, from `src/main/assembly-resources/`. It is *not*
+ under `src/main/resources`, because from there it would be published inside the library jar and land
+ on every consumer's classpath: slf4j-simple reads whichever file the classloader hands it first, so
+ a consumer with their own configuration would get a coin flip. A library must not decide that. The
+ `assembly` profile therefore uses its own descriptor — a verbatim copy of the predefined
+ `jar-with-dependencies` plus that one file.
+
+- **`checker-qual` pinned to 3.55.1 and marked optional.** 4.x is major 55 and its annotations are
+ `@Retention(RUNTIME)`, so a Java 8 JVM throws `UnsupportedClassVersionError` the moment anything
+ reflects over an annotated element. The optional flag keeps it out of consumers' transitive graph;
+ the version pin is what protects the fat jar, since `jar-with-dependencies` filters on scope only.
+ The build-time Checker Framework processor stays on 4.2.2 under its own property.
+
### Added
- **`ModelParameters.setFlashAttn(FlashAttn)` — the only way to express `--flash-attn` correctly.**
llama.cpp turned that option from a bare flag into a value-taking one in **b10273**: the
diff --git a/CLAUDE.md b/CLAUDE.md
index c11ac495..ed50871c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1620,6 +1620,32 @@ EXPECT_FALSE(j.contains("stop_type")); // filtered out
See [`../workspace/policies/javadoc-conventions.md`](../workspace/policies/javadoc-conventions.md).
+## Java 8 bytecode floor — what may ship
+
+This artifact targets **Java 8** (`release 8`), so **every class a consumer's JVM can load must be
+class-file major 52 or lower**. Two entries in `llama/pom.xml` exist only for that, and both are
+easy to undo by accident:
+
+- **`slf4j-simple`, not logback, is the shipped SLF4J binding.** Every logback release from 1.4.0 on
+ is Java 11 bytecode, so `LogbackServiceProvider` cannot load on Java 8 — SLF4J's `ServiceLoader`
+ finds it at startup and the JVM throws `UnsupportedClassVersionError`. The Java 8 line (1.3.x) is
+ end-of-life (last release 1.3.16, 2025-10-29) and every logback CVE disclosed since has been fixed
+ only in 1.5.x/1.6.x with no backport, so it is not an option either. `slf4j-simple` is six classes
+ from the same release train as `slf4j-api`, with no configuration or socket layer for a CVE to
+ live in. Configure it with a classpath `simplelogger.properties` or `-Dorg.slf4j.simpleLogger.*`.
+- **`checker.qual.version` (3.55.1) is a separate property from `checker.version` (the build-time
+ processor).** checker-qual 4.x is major 55, its annotations are `@Retention(RUNTIME)`, and anything
+ reflecting over an annotated element (Jackson does) loads them. `true` keeps
+ it out of consumers' transitive graph but **not** out of the fat jar — `jar-with-dependencies`
+ filters on scope only — so the version pin is what protects the shipped artifact. Never collapse
+ the two properties back into one: the processor runs on the CI JDK and must stay current.
+
+**Surefire excludes `org.slf4j:slf4j-simple` from the test classpath** (`classpathDependencyExcludes`).
+Runtime scope is on the test classpath too, and LogCaptor (test scope) requires logback specifically —
+with both providers present it fails with *"SLF4J Logger implementation should be of the type
+[ch.qos.logback.classic.Logger]"*. The exclusion leaves logback the sole provider in tests and does
+not touch the artifact.
+
## SpotBugs Suppressions
See [`../workspace/policies/spotbugs-suppressions.md`](../workspace/policies/spotbugs-suppressions.md).
@@ -1828,7 +1854,7 @@ the recommended path (README "Importing in Android", Option 1):
change was needed. Built by the **standalone plain-Gradle build** in `llama-android/`
(see "Repository layout" for why it is not a Maven module); the POM mirrors the core's
compile-scope deps (jackson/slf4j-api/jspecify/checker-qual, versions parsed from
- `llama/pom.xml` — deliberately NOT logback, which is the JVM-only runtime binding).
+ `llama/pom.xml` — deliberately NOT the SLF4J binding, which is the JVM-only runtime dependency).
- **`net.ladenthin:llama-kotlin`** — Maven reactor module; pure-Kotlin (2.4, jvmTarget 1.8)
coroutines façade: `generateFlow`/`generateChatFlow` (cold `Flow`, source closed on
completion/error/cancellation) and `completeSuspend`/`chatSuspend`/`chatCompleteTextSuspend`/
diff --git a/llama/pom.xml b/llama/pom.xml
index 6dcad70c..6b0a4d8d 100644
--- a/llama/pom.xml
+++ b/llama/pom.xml
@@ -59,7 +59,14 @@ SPDX-License-Identifier: MIT
1.18.46
2.50.0
0.14.0
+
4.2.2
+
+ 3.55.1
2.22.2
3.8.7
2.0.18
@@ -183,10 +190,20 @@ SPDX-License-Identifier: MIT
jspecify
${jspecify.version}
+
org.checkerframework
checker-qual
- ${checker.version}
+ ${checker.qual.version}
+ true
com.fasterxml.jackson.core
@@ -200,11 +217,34 @@ SPDX-License-Identifier: MIT
${slf4j.version}
+ required on the compile classpath, only loaded at JVM startup.
+
+ slf4j-simple rather than logback, for two independent reasons:
+
+ (1) Java 8. Every logback release from 1.4.0 on is Java 11 bytecode, so
+ LogbackServiceProvider cannot load on the Java 8 this artifact targets:
+ SLF4J's ServiceLoader finds it at startup and the JVM throws
+ UnsupportedClassVersionError. The Java 8 line (1.3.x) would fix that but
+ is end-of-life: 1.3.16 (2025-10-29) is its last release, and every logback
+ CVE disclosed since has been fixed only in 1.5.x/1.6.x with no backport
+ (CVE-2026-1225, CVE-2026-9828, CVE-2026-10532; CVE-2026-19880 is fixed only in 1.6.3,
+ which is Java 11 bytecode and therefore unreachable from here).
+
+ (2) Attack surface. Essentially every logback CVE lives in its configuration
+ or socket layers: Janino expression evaluation, HardenedObjectInputStream,
+ SaxEventRecorder, SocketReceiver. slf4j-simple is six classes with no
+ config parser, no socket server and no deserialization, so those classes
+ of defect cannot exist in it. It also ships in the same release train as
+ slf4j-api above, so the two can never drift apart.
+
+ What consumers lose: no logback.xml. Configure via a classpath
+ simplelogger.properties or -Dorg.slf4j.simpleLogger.* system properties.
+ Anyone who wants logback (or any other binding) excludes this one and
+ declares their own; that is the point of the SLF4J split. -->
- ch.qos.logback
- logback-classic
- ${logback.version}
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
runtime
@{argLine} -Xmx2g -XX:ErrorFile=hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:+EnableDynamicAgentLoading
+
+
+ org.slf4j:slf4j-simple
+
+
+ src/assembly/fat-jar.xml
+
net.ladenthin.llama.server.ServerLauncher
diff --git a/llama/src/assembly/fat-jar.xml b/llama/src/assembly/fat-jar.xml
new file mode 100644
index 00000000..6664c00b
--- /dev/null
+++ b/llama/src/assembly/fat-jar.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ jar-with-dependencies
+
+ jar
+
+ false
+
+
+ /
+ true
+ true
+ runtime
+
+
+
+
+ ${project.basedir}/src/main/assembly-resources
+ /
+
+ simplelogger.properties
+
+
+
+
diff --git a/llama/src/main/assembly-resources/simplelogger.properties b/llama/src/main/assembly-resources/simplelogger.properties
new file mode 100644
index 00000000..7b84829a
--- /dev/null
+++ b/llama/src/main/assembly-resources/simplelogger.properties
@@ -0,0 +1,29 @@
+# SPDX-FileCopyrightText: 2026 Bernard Ladenthin
+#
+# SPDX-License-Identifier: MIT
+
+# slf4j-simple defaults for the RUNNABLE FAT JAR only.
+#
+# This file is deliberately NOT under src/main/resources: that would put it into the
+# published library jar, where every consumer of net.ladenthin:llama would find our
+# logging configuration on their classpath. slf4j-simple reads whichever
+# simplelogger.properties the classloader hands it first, so a consumer with their own
+# file would get a coin flip. A library must not decide that; an application may, and
+# the fat jar is the application.
+#
+# Every setting here is also overridable at launch with -Dorg.slf4j.simpleLogger..
+
+# INFO keeps startup and per-request lines without the native layer's debug chatter.
+org.slf4j.simpleLogger.defaultLogLevel=info
+
+# stdout, not the slf4j-simple default of stderr: the server's own output belongs on the
+# same stream as everything else a user pipes or redirects.
+org.slf4j.simpleLogger.logFile=System.out
+
+# Wall-clock timestamps -- a server log without them is hard to correlate with anything.
+org.slf4j.simpleLogger.showDateTime=true
+org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS
+
+# Thread and short logger name, mirroring what the previous logback default emitted.
+org.slf4j.simpleLogger.showThreadName=true
+org.slf4j.simpleLogger.showShortLogName=true