@uncaged/nerve-daemon
v0.5.0
Published
The observation engine runtime for [nerve](../../README.md) — runs senses, routes signals, schedules reflexes, and manages workflows.
Downloads
1,531
Readme
@uncaged/nerve-daemon
The observation engine runtime for nerve — runs senses, routes signals, schedules reflexes, and manages workflows.
Architecture
| Module | Source (indicative) | Responsibility |
|--------|---------------------|----------------|
| Kernel | kernel.ts | Orchestrator — worker pool, signal bus, reflex 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 |
| Reflex scheduler | reflex-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 reflexes (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-daemonRequires Node.js ≥ 22.5 (for node:sqlite).
License
MIT
