github/copilot-sdk · error · IllegalStateException
FfiRuntimeHost is already closed.
Error message
FfiRuntimeHost is already closed.
What it means
FfiRuntimeHost.start() throws this IllegalStateException when the host has already been disposed (disposed flag set) and cannot be started again. A closed FfiRuntimeHost is terminal; a new instance must be created to serve another in-process runtime.
Solutions
- Create a new FfiRuntimeHost instance instead of calling start on a disposed one
- Track host state in your application and rebuild the host after any close
- Avoid closing the host for transient restarts; only close at full application shutdown
- Check disposed state before start (or catch IllegalStateException) and re-create the host in the recovery path
Example fix
// before host.close(); host.start(entrypoint, options); // IllegalState // after host.close(); host = new FfiRuntimeHost(nativeBinding); host.start(entrypoint, options);
Defensive patterns
Strategy: validation
Validate before calling
// track disposal yourself; FfiRuntimeHost.disposed is internal
if (hostRef.get() == null || hostClosed) {
hostRef.set(new FfiRuntimeHost(nativeBinding));
}
FfiRuntimeHost host = hostRef.get(); Try / catch
try {
host.start(entrypoint, options);
} catch (IllegalStateException e) {
if (String.valueOf(e.getMessage()).contains("already closed")) {
host = new FfiRuntimeHost(nativeBinding);
host.start(entrypoint, options);
} else throw e;
} Prevention
- Treat FfiRuntimeHost as single-use; create a new instance after close
- Only close the host at final application shutdown
- Guard restart flows to rebuild the host, not restart it
- Keep host references out of long-lived singletons that outlive close
When it happens
Trigger: Calling start(entrypointPath, options) on an FfiRuntimeHost instance after close()/dispose completed — e.g., restarting the runtime by re-calling start on the same object instead of constructing a fresh host.
Common situations: Restart-after-shutdown logic that reuses the host reference; lifecycle bugs in tests or apps where close() runs in a finally and start() is later called for the next operation; DI containers caching a disposed host singleton.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- CLI process not started
- CLI child process was unexpectedly started in parent…
- Session not found
- An in-process FFI runtime library is already loaded from
- The in-process runtime connection is closed.
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/88998335b26ffaa1.
Report an issue: GitHub.
Appendix: source
Thrown at java/sdk/src/main/java/com/github/copilot/ffi/FfiRuntimeHost.java:100
private static Path resolveLibraryPath() throws IOException {
return NativeRuntimeLoader.resolve();
}
/**
* Starts the in-process runtime and opens a connection.
*
* @param entrypointPath
* optional explicit legacy CLI entrypoint passed in
* {@code argv_json}
* @param options
* client options used to construct {@code argv_json} and
* {@code env_json}
*/
public void start(String entrypointPath, CopilotClientOptions options) {
Objects.requireNonNull(options, "options must not be null");
if (disposed.get()) {
throw new IllegalStateException("FfiRuntimeHost is already closed.");
}
if (serverId.get() != 0 || connectionId.get() != 0) {
throw new IllegalStateException("FfiRuntimeHost has already been started.");
}
byte[] argvJson = buildArgvJson(entrypointPath, options);
byte[] envJson = buildEnvJson(options);
int hostHandle = runHostStartOnBlockingThread(argvJson, envJson);
if (hostHandle == 0) {
String lib = libraryPath != null ? libraryPath : "<unknown>";
throw new IllegalStateException("copilot_runtime_host_start failed (library '" + lib + "').");
}
// Hold operationLock while publishing handles to serialize with close().
// Recheck disposed in case close() ran while hostStart was blocking.
operationLock.lock();
try {
if (disposed.get()) {View on GitHub (pinned to cd8cf15dc3)