Understanding engine-registry.ts in OpenWork: Persistent Process Tracking and Cleanup
engine-registry.ts serves as the central persistence layer that records every OpenCode engine process to disk, enabling the OpenWork server to safely identify and terminate orphaned processes after crashes or unclean shutdowns.
The engine-registry.ts module in the OpenWork repository solves a critical gap in process lifecycle management. While the server's in-memory ServerConfig tracks active engines during operation, these references vanish when the process exits unexpectedly. This TypeScript module maintains a durable JSON registry that survives crashes, allowing the system to reap stray child processes and maintain system hygiene across server restarts.
Why OpenWork Needs a Persistent Engine Registry
During normal operation, OpenWork's ServerConfig maintains references to active engine processes including their ports and URLs. However, if the server exits uncleanly—whether from a crash, forced shutdown, or interrupted development session—these in-memory references vanish. The child OpenCode engine processes become orphan processes consuming system resources with no parent to manage them.
Desktop builds mitigate this with ps-based process scans, but development builds and Windows environments lack this protection. The registry writes a JSON file (engine-instances.json) to the runtime storage directory with mode 0600, ensuring that process metadata survives crashes and remains accessible only to the owner.
The EngineInstanceRecord Data Structure
Each entry in the registry follows the EngineInstanceRecord type defined at the top of apps/server/src/engine-registry.ts (lines 24-51). This structure captures sufficient metadata to identify and safely terminate processes:
export type EngineInstanceRecord = {
id: string; // unique per spawn
pid: number; // child process id
port: number; // loopback port reported by the engine
url: string; // base URL parsed from the engine's readiness line
startedAt: number;
role: EngineInstanceRole; // "starting" | "primary" | "draining"
serverRunId: string; // unique per OpenWork server boot
ownerPid: number; // PID of the server that spawned the engine
authProbe: string; // Basic-auth header for this spawn
bin: string; // path to the engine binary that was launched
};
The authProbe field stores the Basic authentication header required to communicate with the engine, while ownerPid tracks which server instance created the process. These fields enable the reaper to verify process legitimacy before termination.
Core API for Engine Lifecycle Management
The module exports a minimal surface area designed for safe concurrent access. Writes are serialized through a per-path promise queue (registryWriteQueue) to prevent race conditions when multiple engines spawn simultaneously.
Registering and Updating Engine Instances
Use registerEngineInstance(config, record) to persist a new engine when spawning completes (lines 141-155). This function atomically updates the JSON file, adding or replacing entries by ID.
When transitioning an engine between states, call updateEngineInstanceRole(config, id, role) to modify the role field (lines 160-166). The engine pool uses this to mark engines as "draining" before termination, ensuring traffic routing stops before the process exits.
To remove entries after clean shutdown, use removeEngineInstance(config, id), which deletes the record from disk.
Reading the Registry
The readEngineRegistry(config) function (lines 121-133) loads all entries from engine-instances.json. It handles corrupt JSON gracefully by treating parse errors as empty registries, allowing the server to start clean if the file becomes damaged.
The Reaper: Cleaning Up Orphaned Processes
The reapOrphanEngineInstances(config, options?) function (lines 259-349) implements the critical safety logic invoked during server startup. This reaper prevents resource leaks by terminating stray engines while avoiding collateral damage to unrelated processes.
Before calling killEngineProcess(pid), the reaper validates three safety signals:
- Owner liveness – If the
ownerPidrecorded in the registry is still alive, the engine is considered active and spared. - Process-command match – The current command line of the candidate PID must match the recorded
binpath via thecommandMatchesEnginecheck. - Auth-probe health check – The reaper sends an HTTP request to
/global/healthusing the storedauthProbeheader. A401or403response indicates the port now belongs to a different process, causing the entry to be dropped rather than killed.
Safety Mechanisms and Error Handling
Beyond the reaper's runtime checks, the registry implements multiple defense layers. File writes use mode 0600 to protect the embedded authentication headers from other system users. The promise queue ensures that simultaneous engine spawns cannot corrupt the JSON file through interleaved writes. If the registry file becomes corrupted, the parser returns an empty array and permits the next successful write to overwrite the damage.
Integration with the OpenWork Architecture
The registry sits at the intersection of several critical subsystems:
apps/server/src/engine-pool.ts– Uses the registry to track primary and secondary engines, updating roles during pool scaling operations.apps/server/src/server.ts– InvokesreapOrphanEngineInstancesearly in the startup sequence to guarantee a clean process environment before binding ports.apps/server/src/runtime-db.ts– Provides theruntimeStorageDirpath whereengine-instances.jsonresides.apps/server/src/types.ts– Defines theServerConfiginterface required by all registry functions.
Practical Implementation Examples
Registering a Newly Spawned Engine
When launching an OpenCode engine, capture the process metadata and persist it immediately:
import {
registerEngineInstance,
buildEngineAuthProbeHeader,
type EngineInstanceRecord,
} from "./engine-registry.js";
import type { ServerConfig } from "./types.js";
async function launchEngine(config: ServerConfig, pid: number, port: number, bin: string) {
const record: EngineInstanceRecord = {
id: crypto.randomUUID(),
pid,
port,
url: `http://127.0.0.1:${port}`,
startedAt: Date.now(),
role: "primary",
serverRunId: process.env.OPENWORK_RUN_ID ?? "",
ownerPid: process.pid,
authProbe: buildEngineAuthProbeHeader("engineUser", "enginePass"),
bin,
};
await registerEngineInstance(config, record);
}
Updating an Engine's Role During Drain
When scaling down or rotating engines, update the role to prevent new connections:
await updateEngineInstanceRole(config, engineId, "draining");
Reaping Orphans on Server Startup
Call the reaper during initialization to clean up leftovers from previous crashes:
import { reapOrphanEngineInstances } from "./engine-registry.js";
async function cleanUpOnBoot(config: ServerConfig) {
const result = await reapOrphanEngineInstances(config, {
logger: console,
probeTimeoutMs: 1500,
killWaitMs: 500,
});
console.log("Reap result:", result);
}
Diagnostic Registry Inspection
For debugging or monitoring, read the current registry state:
import { readEngineRegistry } from "./engine-registry.js";
const entries = await readEngineRegistry(config);
entries.forEach(e => {
console.log(`Engine ${e.id} – PID ${e.pid} – Role ${e.role}`);
});
Summary
engine-registry.tsprovides durable persistence for OpenCode engine metadata, solving the orphan process problem that in-memory tracking cannot address.- The EngineInstanceRecord type stores PIDs, ports, auth probes, and binary paths required for safe process identification.
- The reaper implements three-layer safety checks (owner liveness, command matching, and health probing) to prevent killing legitimate processes.
- Serialized writes and restrictive file permissions (
0600) protect registry integrity and sensitive authentication data. - The registry integrates with
engine-pool.tsfor lifecycle management andserver.tsfor crash recovery.
Frequently Asked Questions
What happens to running engines if the OpenWork server crashes?
Without the registry, OpenCode engines become orphaned zombie processes consuming ports and memory. The engine-registry.ts module writes process metadata to engine-instances.json before crashes occur. When the server restarts, reapOrphanEngineInstances reads this file and terminates any engines whose parent PID (ownerPid) no longer exists, reclaiming system resources automatically.
How does engine-registry.ts prevent accidentally killing unrelated system processes?
The reaper implements a three-stage safety protocol. First, it checks if the ownerPid is still alive. Second, it verifies the process command line matches the recorded bin path via commandMatchesEngine. Third, it probes /global/health with the stored authProbe; if the endpoint returns 401 or 403, the port was reassigned to a different service and the entry is dropped without killing the process.
Where is the engine registry file stored on disk?
The registry writes to engine-instances.json within the runtime storage directory provided by runtime-db.ts. The file is created with mode 0600 (read/write owner only) to protect the embedded Basic authentication headers. The exact path depends on the OpenWork installation but typically resides in a platform-specific application data directory.
Which OpenWork components interact with the engine registry?
The engine pool (engine-pool.ts) uses the registry to track primary and secondary engines during scaling operations. The server bootstrap (server.ts) invokes the reaper on startup to ensure clean state. The runtime database (runtime-db.ts) provides the storage path, while types.ts defines the configuration interfaces used throughout the registry API.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →