Když váš AI agent přestane věřit jazykovému serveru: tři chyby za tím šumem

Našeho AI kódovacího agenta jsme uprostřed úkolu přistihli, jak říká tohle:
“Subagent confirmed 32/32 constructions now at 16 args (all diagnostics are codegen/Lombok LSP noise).”
Přečtěte si to ještě jednou. Agent zrefaktoroval metodu v repository, jazykový server vyhodil stěnu chyb a agent — správně, jak se ukázalo — každou jednu z nich odmítl jako falešný poplach a šel dál.
To je ten nejhorší možný stav pro codebase, na němž pracuje agent. Jazykový server, který křičí vlk, je horší než žádný jazykový server, protože agent nejen ztrácí signál — on si osvojuje návyk. Jakmile se “diagnostika je tady šum” párkrát potvrdí, agent mávne rukou i nad tou diagnostikou, která říká pravdu. Celý smysl zapojení LSP do agentního harnessu je v tom, že agent dostává po každé úpravě zpětnou vazbu na úrovni kompilátoru, místo aby objevil rozbití až o pár minut později při plném buildu. Falešné poplachy tento kontrakt potichu ruší.
Tak jsme strávili jeden večer zjišťováním, odkud ten šum pochází. Ukázalo se, že jde o tři nezávislé chyby naskládané na sobě, kde každá maskovala tu další. Jedna z nich je skutečná, neopravená nekompatibilita mezi Eclipse JDT Language Server a Gradle 9, na kterou narazíte u jakéhokoli multiprojektového buildu — to je ten hlavní bod a důvod, proč tento článek vznikl. Pokud jste sem přišli z vyhledávání:
Resolution of the configuration ':<project>:annotationProcessor' was attempted
without an exclusive lock. This is unsafe and not allowed.přeskočte na chybu #3.
Sestava
Codebase je Spring Boot modulit na JDK 25 toolchainu — multiprojektový Gradle build (Gradle 9.3.1) s Lombokem, MapStruct a Netflix DGS GraphQL codegenem, který generuje typy do build/generated/sources/dgs-codegen. Agentní harness je Claude Code s oficiálním pluginem jdtls-lsp, který spouští jakýkoli binárka jdtls, jež je na PATH (ta naše pocházela z Homebrew, verze 1.58.0).
Tři detaily z toho — Lombok, generované zdroje a “spouští, co je zrovna na PATH” — jsou každý jednou ze tří chyb. Nic z toho není exotické; pokud provozujete Gradle projekt s Lombokem pod jdtls (nebo vscode-java, který obaluje tentýž server), nějaká podmnožina tohoto článku se týká i vás.
Bug #1: nikdo neřekl jdtls o Lomboku
Lombok generuje členy v době kompilace pomocí triku „anotační procesor plus agent". Build nástroje to řeší přes konfiguraci annotationProcessor, jenže jazykový server kompiluje kód vlastním vestavěným kompilátorem, uvnitř vlastní JVM. Aby jdtls dokázal resolvovat členy @Getter/@Builder/@Data, musí být Lombok jar připojen jako javaagent k JVM jazykového serveru — přesně to dělá vscode-java, když zapnete podporu Lomboku.
Spouštěcí konfigurace pluginu pro Claude Code je minimální:
"lspServers": {
"jdtls": {
"command": "jdtls",
"extensionToLanguage": { ".java": "java" },
"startupTimeout": 120000
}
}Žádné argumenty, žádný agent, žádná konfigurační páčka. Výsledek: každá Lombokem generovaná metoda v codebase hlášena jako „undefined" — stovky diagnostik čistého šumu, ještě než agent napsal jediný znak.
Oprava využívá toho, že spouštěcí příkaz se resolvuje přes PATH. Oficiální Python launcher jdtls přijímá --jvm-arg=, takže drobný shim dříve na PATH odvede svou práci:
#!/bin/bash
# ~/.local/bin/jdtls — shim pro LSP klienty, kteří spouští holé `jdtls`.
# Připojuje Lombok javaagent, aby server resolvoval generované členy.
LOMBOK_JAR="$HOME/.local/share/lombok/lombok.jar"
if [[ -f "$LOMBOK_JAR" ]]; then
exec /opt/homebrew/bin/jdtls "--jvm-arg=-javaagent:$LOMBOK_JAR" "$@"
fi
exec /opt/homebrew/bin/jdtls "$@"Jeden version gotcha, který stojí za to znát: Lombok agent musí podporovat tu JDK, na které běží jazykový server — a zjistit, která JDK to vlastně je, nás stálo zacházku. Není to toolchain projektu; ten je pinnutý v build.gradle.kts a mění se, až když se tak rozhodnete. A není to nutně ani JDK, která přišla s vaší instalací jdtls: launcher z Homebrew sáhne po své vestavěné OpenJDK jen tehdy, když je JAVA_HOME nenastavené (JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk/...}"), takže v praxi server zdědí cokoli, co váš version manager — mise, SDKMAN, asdf — vyexportoval do shellu, který spustil agentní session. Ta naše nakonec běžela na Temurinu 25 z version manageru, ne na Homebrew JDK, jak jsme nejdřív předpokládali; lsof na procesu serveru to rozhodne, ne historie vaší instalace. Pointa platí tak jako tak: JDK serveru se řídí konfigurací vašeho version manageru, podle vlastního rozvrhu, nezávisle na toolchainu projektu — a v den, kdy přeskočí podporovaný rozsah vašeho Lombok agenta, LSP potichu zase ztratí Lombok. Proto shim záměrně ukazuje na svůj vlastní agentní jar, oddělený od projektové závislosti: pinli jsme 1.18.42 (nejnovější v té době, s opravou parsování javadocu pro JDK 25 nad rámec podpory JDK 25 v 1.18.40), protože novější agent zvládne starší anotace v pohodě, zatímco agent starší než JDK serveru je to, co se rozbíjí. Když váš version manager povýší Javu, zkontrolujte řádek o podpoře JDK v changelogu Lomboku dřív než cokoli jiného.
Ověření bylo uspokojivé: goToDefinition na volání SomeEntity.builder() skočilo rovnou na anotaci @Builder na třídě. To je chování s povědomím o Lomboku — před shimem tentýž požadavek vracel „cannot be resolved".
Bug #2: 120sekundový timeout vs sedmiminutový import (smrtící spirála)
S opraveným Lombokem server pořád neodpovídal. Plugin dává jdtls 120 sekund na inicializaci. Studený import našeho monorepa — build Gradle modelu, resolvování závislostí, indexování zdrojů přes tucet podprojektů — trvá déle. O kolik déle? Log workspace odpověděl přesně:
!MESSAGE Workspace initialized in 437755ms7,3 minuty. Timeout se spustí po 2 minutách. Ale tady je ta část, která mění pomalý start ve smrtící spirálu: když to klient vzdá a odpojí se, osiřelý server běží dál — a dál drží zámek na workspace. Další LSP požadavek spustí nový server, který nedokáže získat zamčený workspace, takže mu launcher přidělí zbrusu nový adresář workspace… který spustí ten sedmiminutový import od nuly. Opakovat donekonečna.
Žádný jednotlivý pokus nemůže nikdy uspět a celé to vypadá jako „LSP je prostě nestabilní". Když jsme se nakonec podívali do tabulky procesů, našli jsme instance jdtls, které běžely přes týden, každá vlastnila jiný napůl naimportovaný workspace.
Dvě opravy:
- Zvyšte timeout nad dobu studeného importu (šli jsme na 600 s). V případě Claude Code to znamenalo upravit manifest pluginu — žádné vystavené nastavení neexistuje, což bychom označili za nedostatek: pevný rozpočet 120 s předpokládá, že každý Java projekt na světě se naimportuje za dvě minuty. Pozor: tato úprava žije v naklonovaném manifestu marketplace, takže aktualizace pluginu ji potichu vrátí zpět — znovu aplikujte, pokud se 120s timeouty vrátí.
- Provozně přerušte spirálu jednou ručně: nechte jednoho osiřelého spustit import na pozadí (sledujte, až se CPU uklidní), zabijte všechny serverové procesy a připojte se znovu. Teplý workspace udělá další inicializaci pohodlně rychlou.
Když už jsme u toho byli, vymazali jsme i nahromaděné cache workspaců. Per-adresářové workspaces serveru posbíraly Eclipse projekty ukazující na git worktree, které už neexistovaly, a každá událost o změně zdrojů o ně zakopla („Can’t read root project location…"). Pokud váš agentní workflow vytváří a ničí worktree — náš to dělá pořád — tyto mrtvoly se hromadí ve workspace jazykového serveru, dokud ho nevyčistíte.
Bug #3: Gradle 9 rozbil init skript jdtls pro anotační procesory
Tohle je ta, která přežije obě opravy výše, ta bez fixu upstream, a ta, na kterou narazíte na jakémkoli editoru postaveném na jdtls — včetně vscode-java — v den, kdy váš multiprojektový build přejde na Gradle 9.
Symptom: každý typ z generátoru kódu (v našem případě výstup Netflix DGS codegenu pod build/generated/sources/dgs-codegen) je neresolvovatelný. Importy generovaných balíčků se rozsvítí jako chyby napříč celou codebase, navzdory tomu, že adresář existuje, je zaregistrovaný v sourceSets.main.java.srcDirs a z příkazové řádky se kompiluje bez problému.
Mechanismus: jdt.ls injektuje do každého syncu vlastní Gradle init skript (gradle/apt/init.gradle, zabalený uvnitř serveru). Skript registruje ToolingModelBuilder, který prochází všechny projekty od kořene a čte konfiguraci anotačních procesorů u každého z nich:
FileCollection apPath = options.getAnnotationProcessorPath()
if (apPath != null) {
processors.addAll(apPath.getFiles()) // resolvuje konfiguraci
}Toto volání getFiles() resolvuje konfiguraci annotationProcessor jiného projektu, a to z model builderu kořenového projektu. Gradle 9 tady vynucuje izolaci projektů a odmítá:
Caused by: org.gradle.api.internal.artifacts.configurations.DefaultConfiguration$IllegalResolutionException:
Resolution of the configuration ':auth-server:annotationProcessor' was attempted
without an exclusive lock. This is unsafe and not allowed.Výjimka zabije celý model fetch, selhání model fetche přeruší refresh classpath jazykového serveru a právě tento refresh classpath by zaregistroval složku s generovanými zdroji. Takže viditelná oběť (neresolvovatelné generované typy) je tři kroky vzdálená od skutečného zločinu (jedna konfigurace resolvovaná bez zámku) — proto nám hledání zabralo večer.
Zkontrolovali jsme master jdt.ls v naději na backport opravy: skript je tam byte po bytu identický s tím zabaleným. V době psaní tohoto článku se chyba dodává v poslední vydané verzi.
Předchozí práce a proč je to stále neopravené. Bylo to nahlášeno jako eclipse.jdt.ls #3505 krátce po Gradle 9.0.0 — stejný skript, stejný řádek, stejná výjimka. Reportér ji sám zavřel poté, co zjistil, že org.gradle.parallel=false chybu odstraní, a navazující otázka ve vlákně („je to tedy chyba Gradle, nebo LSP?") zůstala bez odpovědi. Takže issue je zavřené, skript nezměněný a kdokoli s paralelním multiprojektovým buildem — což je konfigurace, kterou chcete u každého buildu dost velkého na to, aby na tom záleželo — na to stále naráží. To je i náš spouštěč: org.gradle.parallel=true sedí v našem gradle.properties, jak má. A v agentním světě nejsme sami: Serena, další LSP-pro-agenty toolkit, narazil na identický pád a sáhl po „nechme uživatele úplně vypnout zpracování anotací" jako po únikovém východu.
To vám dává tři úrovně workaroundu, sestupně podle ceny:
org.gradle.parallel=falsevgradle.properties— opraví import tím, že zpomalí každý build každému vývojáři na projektu. Chyba jazykového serveru by neměla danit vaše CI.--no-parallelpřesjava.import.gradle.jvmArguments— omezí cenu na samotné syncy jazykového serveru, ale vyžaduje klienta, který nastavení předává do jdt.ls. vscode-java to umí; headless launchery (plugin Claude Code, Serena) to aktuálně neumějí.- Zapatchovat zabalený skript tak, aby model builder degradoval elegantně — to jsme udělali my.
Protože zavřený report nechal chybu bez vlastníka, založili jsme eclipse.jdt.ls #3807 s analýzou hlavní příčiny, mitigací níže a nabídkou poslat PR s pořádnou opravou bezpečnou vůči zámkům.
Prozatímní oprava je hrubá, ale ohraničená — obalí per-projektový sběr do try/catch, takže selhání zámku jednoho projektu degraduje na „žádné info o anotačních procesorech pro ten projekt" místo přerušení celého modelu:
private void collectApConfiguration(project, compileTaskName, processors, compilerArgs) {
// Gradle 9 zakazuje resolvovat konfiguraci annotationProcessor jiného projektu
// bez jeho state locku (IllegalResolutionException). Degraduj na "žádné AP info" pro
// ten projekt místo selhání celého model fetche, který přeruší
// refresh classpath pro každý projekt v buildu.
try {
JavaCompile javaCompile = project.getTasks().findByName(compileTaskName)
if (javaCompile != null) {
CompileOptions options = javaCompile.getOptions()
if (!options.compilerArgs.contains("-proc:none")) {
FileCollection apPath = options.getAnnotationProcessorPath()
if (apPath != null) {
processors.addAll(apPath.getFiles())
}
compilerArgs.addAll(options.getCompilerArgs())
}
}
} catch (Throwable t) {
// ignoruj — projekt si nechá výchozí konfiguraci zpracování anotací
}
}Skript žije uvnitř extrahované OSGi bundle cache serveru, kde adresář, který ho obsahuje, má proměnlivý číselný segment. Najděte si ho, místo hádání:
find "$(brew --prefix jdtls)/libexec/configuration" -path '*apt*' -name init.gradle
# …/libexec/configuration/org.eclipse.osgi/<n>/0/.cp/gradle/apt/init.gradlePatch je lokální na daném stroji a upgrady ho vrátí zpět — znovu aplikujte po brew upgrade jdtls. Pořádná oprava patří upstream do eclipse.jdt.ls (sledováno v #3807); skript musí sbírat per-projektové AP info způsobem bezpečným vůči Gradle 9, místo aby sahal přes hranice projektů.
Po patchi: sync doběhne, model fetch neloguje žádné chyby a — okamžik, ke kterému jsme směřovali — kompilátor jazykového serveru resolvuje každý generovaný typ. Hover na generované třídě vrátí její plné FQN a Javadoc, načtené z generovaného souboru. Diagnostika na souborech importujících generované typy: nula.
Forenzní poznámka: když hover funguje, ale goToDefinition ne
Jeden zbytkový vrtoch stojí za zdokumentování, protože nás málem poslal do čtvrté králičí nory. Po všem výše hover na generovaný typ resolvuje dokonale — ale goToDefinition a workspaceSymbol do generované složky nevrátí nic.
Vypadalo to, že classpath je pořád rozbitá. Není, a způsob, jakým jsme to dokázali, se dá zobecnit: kontrolujte, co vidí kompilátor, ne co vidí navigace. Vlastní výstup buildu serveru (bin/main/... pro jdtls) obsahoval čerstvě zkompilované .class soubory pro generované typy, otimestampované pár sekund po syncu — což znamená, že zdrojová složka je živá, bindingy se resolvují a diagnostika (která pochází z kompilační vrstvy) je důvěryhodná. To, že indexem podpořené operace selhávají na zdrojových složkách pod build/, je samostatné, dlouhodobě známé omezení jdt.ls — pro naše účely kosmetické, protože hover karta nese klikatelný odkaz na přesný soubor.
Ten rozdíl je pro debugging důležitý: diagnostika a hover pocházejí z bindingů; goToDefinition a vyhledávání symbolů pochází z indexu. Pokud hover resolvuje typ, vaše classpath je v pořádku, tečka — nenechte se mrtvým navigačním požadavkem přesvědčit o opaku.
Co to znamená, když zapojujete LSP do agenta
Ta obecná lekce není o Javě. Je o tom, že agentní harness mění ekonomiku spolehlivosti jazykového serveru:
- Člověk falešné poplachy toleruje; agent z nich generalizuje. Vývojář vidí vlnovky na Lombok getterech, povzdechne si a přečte je. Agent si vystaví politiku: „diagnostika v tomhle repu lže." Ta politika pak sežere i ty pravdivé. Pokud transkripty vašeho agenta obsahují fráze jako „LSP noise" — to není agent, který je nedbalý, to je váš toolchain, který ho učí naučené bezmoci.
- Headless spouštěcí cesty přeskakují péči, kterou poskytuje IDE. vscode-java připojuje Lombok agenta, spravuje životní cyklus workspaců a vyhazuje chyby syncu do UI panelu. Holé
jdtlsna PATH nedostane nic z toho a chyby skončí v log souboru, který nikdo nečte (~/Library/Caches/jdtls/<workspace>/.metadata/.logse nám vyplatil každou minutou, kterou jsme v něm strávili). - Timeouty musí sednout projektu, ne ukázce. Jakýkoli pevný startovací rozpočet bude pro něčí monorepo špatně. A pokud klient server navíc opustí, ale nezabije, příliš krátký timeout nejen selže — aktivně vyrábí tu spirálu osiřelých zámků.
- Ověřujte tu vrstvu, na které skutečně závisíte. Pro agenta je to diagnostika — kompilační vrstva. Dokažte ji kontrolami na úrovni bindingů (hover, čerstvý výstup buildu) a nepropalte večer kvůli tomu, že symbolový index má názory na adresáře
build/.
Výplata je reálná: se všemi třemi opravenými chybami dostává agent poctivou zpětnou vazbu kompilátoru pár sekund po každé úpravě, a to v codebase, kde Lombok a GraphQL codegen předtím dělaly LSP nepoužitelným. Diagnostika je teď nudná. Nuda byla cíl.
Verze: jdtls 1.58.0 (Eclipse JDT.LS), Gradle 9.3.1, Lombok 1.18.42 (agent) / 1.18.40 (projekt), JDK 25 (toolchain projektu i JVM serveru), Netflix DGS codegen 8.3.0, Claude Code s pluginem jdtls-lsp. Původní upstream report je eclipse.jdt.ls #3505 (zavřený s workaroundem org.gradle.parallel=false místo opravy); aktivní navazující issue s analýzou a patchem z tohoto článku je eclipse.jdt.ls #3807. Pokud se to vyřeší upstream, preferujte tu opravu před lokálním patchem.
Titulní foto: Michael Dziedzic na Unsplash.


