-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Description
Summary
In Java monorepos (e.g. Gradle multi-module projects), opencode spawns a separate jdtls process per submodule that the agent visits. Each process uses ~200-300MB of RSS, so a session where the agent explores 5 submodules accumulates ~1.2GB of jdtls processes. Across multiple terminal sessions this quickly leads to OOM.
Root cause
The JDTLS root function uses NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]) (server.ts:1132). This walks up from the file being edited and returns the nearest directory containing a build file.
In a monorepo, every submodule has its own build.gradle or pom.xml, so each submodule the agent touches resolves to a different root. Since LSP client deduplication keys on root + serverID (index.ts:231), each unique root spawns a new jdtls process.
This is unnecessary — jdtls natively supports multi-module projects. Pointing it at the monorepo root (where settings.gradle or gradlew lives) lets it discover and handle all submodules with a single process.
Observed behavior
On a Gradle monorepo with ~100 submodules, 2 terminal sessions accumulated 5 jdtls processes:
PID 6789 → cwd: entry/foo
PID 9070 → cwd: entry/bar/qux
PID 13287 → cwd: entry/bar/corge
PID 13292 → cwd: entry/foo (duplicate, different session)
PID 17572 → cwd: entry/baz
Each has a unique -data temp dir and ~200-300MB RSS.
Expected behavior
A single jdtls process rooted at the monorepo root, regardless of how many submodules the agent visits.
Suggested fix
Give JDTLS the same root-finding strategy that KotlinLS already uses (server.ts:1234-1245) — prefer settings.gradle / settings.gradle.kts / gradlew (monorepo root markers) before falling back to build.gradle / pom.xml:
root: async (file) => {
const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file)
if (settingsRoot) return settingsRoot
const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file)
if (wrapperRoot) return wrapperRoot
return NearestRoot(["build.gradle.kts", "build.gradle", "pom.xml", ".project", ".classpath"])(file)
},Related
Related to #7227 (LSP spawning for external directories) — both issues stem from the same pattern of spawning redundant LSP servers when a single instance would suffice.
Environment
- opencode v1.1.43 (Homebrew)
- macOS, Java 21