fix: harden state persistence (follow-up #313) #314
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user