fix: harden state persistence (follow-up #313) #314

Merged
xiaomo merged 1 commits from fix/313-state-persistence-hardening into main 2026-05-01 12:03:31 +00:00
3 changed files with 26 additions and 7 deletions
+13
View File
@@ -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/<name>.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
+1 -1
View File
@@ -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<string>;
};
/**
+12 -6
View File
@@ -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) {