e67ddc58d8
1. trySendSync: wrap child.send in try/catch — IPC race between connected check and send 2. gracefulStop: same try/catch for shutdown send 3. Remove crashTimestamps reset on ready — crash window detection was being bypassed
@uncaged/nerve-daemon
The observation engine runtime for nerve — runs senses, routes signals, runs the sense scheduler, and manages workflows.
Architecture
| Module | Source (indicative) | Responsibility |
|---|---|---|
| Kernel | kernel.ts |
Orchestrator — worker pool, signal bus, sense scheduler, workflow manager, optional file watcher and daemon IPC, config reload hooks |
| Worker pool | worker-pool.ts |
Fork and supervise one child process per sense group; restart/shutdown; crash cleanup hooks for scheduler state |
| Kernel sense groups | kernel-sense-groups.ts |
Derive sense groups from config; list senses per group for scheduling |
| Sense runtime | sense worker + Drizzle | Per-sense SQLite (node:sqlite), migrations, peer DB reads |
| Sense worker | sense-worker.ts (fork target) |
Child process entry — runs compute() per sense in a group |
| Signal bus | signal-bus.ts |
In-memory pub/sub for sense signals |
| Sense scheduler | sense-scheduler.ts |
Interval + on subscriptions, throttle/coalesce |
| Workflow manager | workflow-manager.ts |
One worker per workflow name, concurrency (drop/queue), queue caps |
| Workflow worker | workflow-worker.ts |
Child process — runs RFC-002 threads (start-thread, resume-thread IPC) |
| IPC (parent ↔ workers) | ipc.ts |
Typed messages for sense and workflow workers (includes resume-thread for recovery) |
| Log / workflow persistence | via @uncaged/nerve-store |
Structured logs, workflow_runs, thread messages (used for recovery) |
| Blob store | @uncaged/nerve-store |
CAS under data/blobs/ — sense workers construct createBlobStore(join(nerveRoot, "data", "blobs")) for artifact writes |
| File watcher | file-watcher.ts |
Watches workspace paths for config / sense / workflow file changes |
| Kernel file watch | kernel-file-watch.ts |
Maps watcher events to reloadConfig, sense group restart, workflow drainAndRespawn |
| Daemon IPC | daemon-ipc.ts |
Unix socket server — parses @uncaged/nerve-core DaemonIpcRequest, dispatches trigger-workflow / trigger-sense / list-senses |
Crash recovery (workflow workers)
If a workflow worker exits unexpectedly while threads are active:
- In-flight runs are marked
crashedin the log store; the manager respawns a fresh worker. - Runs still in
startedstate can beresume-thread’d: the manager rebuilds the message chain from persisted workflow log rows and sendsresume-threadto the new worker. - Crash-loop backoff: repeated crashes for the same workflow name are counted in a sliding window (
60s); after5crashes in that window, the manager stops respawning that worker and logs the condition (avoids tight crash loops).
Hot reload (drainAndRespawn) uses a controlled drain: in-flight runs may be marked interrupted when the old worker is torn down after a timeout — that path is distinct from unexpected crash recovery.
Key Design Decisions
- One worker process per sense group — isolation between groups, shared compute within a group
node:sqlite(DatabaseSync) — zero native addons, WAL mode, built into Node.js ≥ 22.5- Throttle + coalesce — if compute is in-flight, at most one pending trigger is queued (no unbounded accumulation)
- Log ≠ Signal — logs are queryable data assets but cannot trigger the sense scheduler or workflows (prevents feedback loops)
Usage
The daemon is typically started via the CLI (nerve daemon start / nerve dev), but you can embed the kernel:
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parseNerveConfig } from "@uncaged/nerve-core";
import { createKernel } from "@uncaged/nerve-daemon";
const nerveRoot = "/path/to/workspace";
const yamlPath = join(nerveRoot, "nerve.yaml");
const parsed = parseNerveConfig(readFileSync(yamlPath, "utf8"));
if (!parsed.ok) {
throw parsed.error;
}
const kernel = createKernel(parsed.value, nerveRoot, {
enableFileWatcher: true,
ipcSocketPath: join(nerveRoot, "nerve.sock"),
});
await kernel.ready;
kernel.triggerSense("cpu-usage");
const health = kernel.getHealth();
await kernel.stop();
createKernel(config, nerveRoot, options?) — config is a parsed NerveConfig; nerveRoot is the workspace root (contains nerve.yaml, data/, etc.). Optional KernelOptions:
| Field | Meaning |
|---|---|
workerScript |
Override path to the sense worker entry script (defaults to the package’s resolved worker) |
enableFileWatcher |
Watch config / senses / workflows for hot reload |
logStore |
Inject a LogStore instance (defaults to createLogStore(join(nerveRoot, "data", "logs.db"))) |
ipcSocketPath |
When non-null, listen for daemon IPC on this Unix socket path |
Install
pnpm add @uncaged/nerve-daemon
Requires Node.js ≥ 22.5 (for node:sqlite).
License
MIT