# @uncaged/nerve-daemon The observation engine runtime for [nerve](../../README.md) — runs senses, persists JSON state, runs the sense scheduler, and manages workflows. ## Architecture | Module | Source (indicative) | Responsibility | |--------|---------------------|----------------| | **Kernel** | `kernel.ts` | Orchestrator — worker pool, 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-runtime.ts` + sense worker | Loads user modules (`compute`, `initialState`), reads/writes `data/senses/.json` | | **Sense worker** | `sense-worker.ts` (fork target) | Child process entry — runs `compute(state)` per sense in a group | | **Sense scheduler** | `sense-scheduler.ts` | Interval + `on` subscriptions (reverse-index by upstream sense), 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/` — workflows use blob storage for artifacts as configured | | **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 **`crashed`** in the log store; the manager respawns a fresh worker. - Runs still in **`started`** state can be **`resume-thread`**’d: the manager rebuilds the message chain from persisted workflow log rows and sends `resume-thread` to the new worker. - **Crash-loop backoff:** repeated crashes for the same workflow name are counted in a sliding window (`60s`); after **`5`** crashes 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 - **Sense state as JSON** — `data/senses/.json`, updated after each successful compute in the worker - **Throttle + coalesce** — if compute is in-flight, at most one pending trigger is queued (no unbounded accumulation) - **Log ≠ Sense trigger** — logs are queryable data assets but cannot schedule sense computes 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: ```typescript 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 ```bash pnpm add @uncaged/nerve-daemon ``` Requires Node.js ≥ 22.5 (for `node:sqlite` in the log store and related persistence). ## License MIT