Embedded Java Process

Java applications can run Memory Service as an owned subprocess without requiring a separate installation. The process API extracts a packaged executable, starts a local SQLite service over a protected Unix domain socket, waits for readiness when it can discover the management endpoint, and stops the child when closed.

This integration is subprocess-based, not JNI. It currently supports Linux AMD64, Linux ARM64, and macOS ARM64. Windows and Intel macOS are not packaged.

Choose a dependency

For a platform-specific application or distribution, depend only on its native JAR. The native JAR also brings in the memory-service-process API:

<dependency>
<groupId>io.github.chirino.memory-service</groupId>
<artifactId>memory-service-binary-linux-amd64</artifactId>
<version>999-SNAPSHOT</version>
</dependency>

The other platform artifact IDs are memory-service-binary-linux-arm64 and memory-service-binary-macos-arm64.

Applications distributed to all supported platforms can use the small aggregate JAR. Its POM depends on all three native JARs:

<dependency>
<groupId>io.github.chirino.memory-service</groupId>
<artifactId>memory-service-binaries</artifactId>
<version>999-SNAPSHOT</version>
</dependency>

Use memory-service-process alone when your application supplies the executable explicitly or finds it on PATH.

Start a local service

import io.github.chirino.memoryservice.process.MemoryServiceProcess;
import java.nio.file.Path;

Path stateDirectory = Path.of(System.getProperty("user.home"), ".my-app", "memory");

try (MemoryServiceProcess service = MemoryServiceProcess.builder(stateDirectory).start()) {
    String target = service.target(); // unix:///.../memory.sock
    // Configure your Memory Service client with target.
}

The builder creates memory.db and memory.sock below the state directory. It configures SQLite, local Unix-socket authentication, no embedding provider, and plain local encryption. These defaults are intended for a single-user local process, not a remote production deployment.

The child command is only memory-service serve. Server configuration is supplied through MEMORY_SERVICE_* environment variables, using the same configuration surface as container deployments:

Default environment variableValue
MEMORY_SERVICE_PLAIN_TEXTtrue
MEMORY_SERVICE_TLSfalse
MEMORY_SERVICE_DB_KINDsqlite
MEMORY_SERVICE_DB_URL<state-directory>/memory.db
MEMORY_SERVICE_UNIX_SOCKET<state-directory>/memory.sock
MEMORY_SERVICE_UNIX_SOCKET_AUTHlocal
MEMORY_SERVICE_EMBEDDING_KINDnone
MEMORY_SERVICE_ENCRYPTION_ALLOW_PLAINtrue
MEMORY_SERVICE_MANAGEMENT_ON_MAIN_LISTENERtrue

Startup normally returns only after GET /ready succeeds. A startup timeout or early child exit throws MemoryServiceStartException with recent child output. close() is idempotent and first requests a graceful stop, then forcibly terminates a child that exceeds the shutdown timeout.

Configure the server

Builder environment entries override the local defaults, so any serve option with a MEMORY_SERVICE_* equivalent can be configured without changing the child command:

import java.util.Map;

MemoryServiceProcess process = MemoryServiceProcess.builder(stateDirectory)
    .environment(Map.of(
        "MEMORY_SERVICE_EMBEDDING_KIND", "local",
        "MEMORY_SERVICE_VECTOR_KIND", "sqlite"))
    .start();

databasePath(Path) and socketPath(Path) are typed conveniences that configure MEMORY_SERVICE_DB_URL and MEMORY_SERVICE_UNIX_SOCKET. The effective listener environment controls target() and readiness probing, so changing listeners through the typed methods or environment(...) remains consistent.

Use an HTTP listener

Call httpListener(...) to disable the default Unix socket and select a plaintext HTTP listener. TCP access does not use local Unix-socket identity, so configure the server’s normal API-key or OIDC authentication as well:

MemoryServiceProcess process = MemoryServiceProcess.builder(stateDirectory)
    .httpListener("127.0.0.1", 8082)
    .environment("MEMORY_SERVICE_API_KEYS_MY_APP", apiKey)
    .start();

String target = process.target(); // http://127.0.0.1:8082

httpListener(int) uses 127.0.0.1. disableUnixSocket() only removes the managed-process Unix socket settings and leaves the final HTTP host, port, plaintext, and TLS values to the server configuration. For a non-loopback plaintext host, explicitly set MEMORY_SERVICE_ALLOW_NON_LOOPBACK_PLAINTEXT=true.

unixSocketPath() returns an Optional<Path> for code that supports either transport. The older socketPath() convenience throws IllegalStateException when HTTP is selected.

Configure readiness

The process API chooses its readiness checker from the final child environment after defaults, inherited values, removals, and explicit overrides have all been applied:

  • With MEMORY_SERVICE_MANAGEMENT_ON_MAIN_LISTENER=true, it probes the selected main Unix-socket or HTTP listener.
  • A configured MEMORY_SERVICE_MANAGEMENT_UNIX_SOCKET or explicit MEMORY_SERVICE_MANAGEMENT_PORT selects that dedicated management listener.
  • managementHttpListener(...) configures a dedicated plaintext management HTTP listener and disables management on the main listener.
  • If management is not on the main listener and neither a management Unix socket nor an explicit, usable management port is configured, no readiness address is knowable. start() launches the child and returns without waiting. The server can still exit later if its own configuration is invalid.

For example, the following probes readiness on port 9090 while clients connect on port 8082:

MemoryServiceProcess process = MemoryServiceProcess.builder(stateDirectory)
    .httpListener(8082)
    .managementHttpListener(9090)
    .environment("MEMORY_SERVICE_API_KEYS_MY_APP", apiKey)
    .start();

The builder normally removes inherited MEMORY_SERVICE_* entries before applying its defaults, which prevents the parent shell from unexpectedly changing the managed child. Use inheritMemoryServiceEnvironment(true) to let inherited entries replace defaults. Explicit environment(...) entries always have the highest precedence. Unrelated inherited variables such as PATH and HOME are retained. removeEnvironment(name) removes either an inherited or default entry; a later environment(name, value) restores it.

Route process output

By default, stdout and stderr are merged into one bridge and sent to Java util logging at FINE. Supply consumers to integrate with application logging:

import java.util.function.Consumer;

Consumer<String> output = line -> applicationLogger.info(line);

MemoryServiceProcess process = MemoryServiceProcess.builder(stateDirectory)
    .standardOutput(output)
    .standardError(output)
    .start();

When both methods receive the same non-null consumer instance, stderr is merged into stdout and one bridge thread is used. Passing null to either method redirects that stream to the operating system’s null device; passing null to both creates no bridge threads.

Control binary resolution

Packaged resolution is the default. You can replace it with an ordered resolver list:

import io.github.chirino.memoryservice.process.MemoryServiceBinary;

MemoryServiceProcess process = MemoryServiceProcess.builder(stateDirectory)
    .binaryResolvers(
        MemoryServiceBinary.file(Path.of("/opt/my-app/memory-service")),
        MemoryServiceBinary.packaged(),
        MemoryServiceBinary.onPath("memory-service"))
    .start();

A resolver falls through only when its source is unavailable. If a candidate exists but is corrupt, cannot be extracted, or cannot be launched, startup fails instead of silently selecting a different binary.

Packaged executables are checksum-verified and cached by platform and SHA-256. The default cache is ~/.cache/memory-service/binaries on Linux and ~/Library/Caches/memory-service/binaries on macOS. Use cacheDirectory(Path) to override it.

The builder also provides typed configuration for startup and shutdown timeouts, the packaged binary cache, binary resolution, output consumers, and the JVM shutdown hook.

Executable JARs and shading

Platform discovery uses Java ServiceLoader. If you build an uber-JAR with Maven Shade, merge service descriptors so the native provider registration is retained:

<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />

Other fat-JAR tools need their equivalent service-file merge behavior for META-INF/services/io.github.chirino.memoryservice.process.MemoryServiceBinaryProvider.