diff --git a/CLAUDE.md b/CLAUDE.md index b76440e..caef987 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,19 @@ External World → Sense(state) → { newState, workflow? } → Workflow → Log +### Sense State Persistence + +Each sense's state is persisted as a JSON file at `data/senses/.json` (relative to the nerve root, typically `~/.uncaged-nerve/`). + +| Event | Behavior | +|-------|----------| +| **Worker start** | Read `state.json`; if missing or corrupt, use `initialState` from the sense module | +| **Compute success** | Write new state atomically (write-temp + rename), then update in-memory state | +| **Compute failure** | State unchanged (both disk and memory) | +| **Daemon restart** | State restored from last successful write | + +State files are written atomically (temp file + rename) to prevent corruption on crash. + ## Language & Paradigm ### Functional-first diff --git a/packages/core/src/sense.ts b/packages/core/src/sense.ts index 19ceb3e..0ba02a1 100644 --- a/packages/core/src/sense.ts +++ b/packages/core/src/sense.ts @@ -8,7 +8,7 @@ export type SenseInfo = { throttle: number | null; timeout: number | null; /** Declarative schedule (`interval` / `on`) for this sense (derived from nerve.yaml). */ - triggers: string[]; + triggers: ReadonlyArray; }; /** diff --git a/packages/daemon/src/sense-runtime.ts b/packages/daemon/src/sense-runtime.ts index 6b38303..e5af5c3 100644 --- a/packages/daemon/src/sense-runtime.ts +++ b/packages/daemon/src/sense-runtime.ts @@ -1,5 +1,5 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname } from "node:path"; +import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; import type { Result, SenseComputeFn, WorkflowTrigger } from "@uncaged/nerve-core"; import { err, isPlainRecord, ok } from "@uncaged/nerve-core"; @@ -14,16 +14,22 @@ export type SenseRuntime = { export function readState(statePath: string, initialState: unknown): unknown { try { + if (!existsSync(statePath)) return initialState; const raw = readFileSync(statePath, "utf8"); return JSON.parse(raw); - } catch { + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + process.stderr.write(`[sense-runtime] warning: failed to read state from "${statePath}": ${msg} — using initialState\n`); return initialState; } } export function writeState(statePath: string, state: unknown): void { - mkdirSync(dirname(statePath), { recursive: true }); - writeFileSync(statePath, JSON.stringify(state, null, 2)); + const dir = dirname(statePath); + mkdirSync(dir, { recursive: true }); + const tmp = join(dir, `.${Date.now()}.tmp`); + writeFileSync(tmp, JSON.stringify(state, null, 2)); + renameSync(tmp, statePath); } /** @@ -86,8 +92,8 @@ export async function executeCompute( ? await Promise.race([computePromise, timeoutPromise]) : await computePromise; - runtime.state = result.state; writeState(runtime.statePath, result.state); + runtime.state = result.state; return ok(result); } catch (e) {