# Understanding engine-registry.ts in OpenWork: Persistent Process Tracking and Cleanup

> Discover how engine-registry.ts in OpenWork provides persistent process tracking and cleanup. Safely identify and terminate orphaned processes after crashes for robust application stability.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-17

---

**[`engine-registry.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/apps/server/src/engine-registry.ts) (lines 24-51). This structure captures sufficient metadata to identify and safely terminate processes:

```ts
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`](https://github.com/different-ai/openwork/blob/main/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:

1. **Owner liveness** – If the `ownerPid` recorded in the registry is still alive, the engine is considered active and spared.
2. **Process-command match** – The current command line of the candidate PID must match the recorded `bin` path via the `commandMatchesEngine` check.
3. **Auth-probe health check** – The reaper sends an HTTP request to `/global/health` using the stored `authProbe` header. A `401` or `403` response 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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/apps/server/src/server.ts)** – Invokes `reapOrphanEngineInstances` early in the startup sequence to guarantee a clean process environment before binding ports.
- **[`apps/server/src/runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/runtime-db.ts)** – Provides the `runtimeStorageDir` path where [`engine-instances.json`](https://github.com/different-ai/openwork/blob/main/engine-instances.json) resides.
- **[`apps/server/src/types.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/types.ts)** – Defines the `ServerConfig` interface 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:

```ts
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:

```ts
await updateEngineInstanceRole(config, engineId, "draining");

```

### Reaping Orphans on Server Startup

Call the reaper during initialization to clean up leftovers from previous crashes:

```ts
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:

```ts
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.ts`](https://github.com/different-ai/openwork/blob/main/engine-registry.ts)** provides 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.ts`](https://github.com/different-ai/openwork/blob/main/engine-pool.ts) for lifecycle management and [`server.ts`](https://github.com/different-ai/openwork/blob/main/server.ts) for 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`](https://github.com/different-ai/openwork/blob/main/engine-registry.ts) module writes process metadata to [`engine-instances.json`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/engine-instances.json) within the runtime storage directory provided by [`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/engine-pool.ts)) uses the registry to track primary and secondary engines during scaling operations. The **server bootstrap** ([`server.ts`](https://github.com/different-ai/openwork/blob/main/server.ts)) invokes the reaper on startup to ensure clean state. The **runtime database** ([`runtime-db.ts`](https://github.com/different-ai/openwork/blob/main/runtime-db.ts)) provides the storage path, while [`types.ts`](https://github.com/different-ai/openwork/blob/main/types.ts) defines the configuration interfaces used throughout the registry API.