feat: guard defs table-only, lifecycle guard restored, daemon fault-tolerant
CI / test (push) Has been cancelled

This commit is contained in:
2026-04-19 02:35:41 +00:00
parent 3a4f4efdf1
commit 75dc04a51d
4 changed files with 26 additions and 24 deletions
+6 -2
View File
@@ -170,8 +170,12 @@ const scheduleNext = () => {
timer = setTimeout(async () => {
try {
await logTick();
} catch (err) {
console.error('❌ Tick error:', err);
} catch (err: any) {
if (err?.name === 'GuardViolationError' || err?.constructor?.name === 'GuardViolationError') {
console.warn(`⚠️ Guard violation (non-fatal): ${err.message}`);
} else {
console.error('❌ Tick error:', err);
}
}
scheduleNext();
}, currentTickMs);
+7 -19
View File
@@ -46,14 +46,9 @@ export function initGuardSchema(db: Database): void {
db.exec(GUARD_SCHEMA);
}
// ── Memory cache ───────────────────────────────────────────────
const guardDefMemory = new Map<string, GuardProjectionDef>();
/** @internal Clear expression cache and memory cache (for testing) */
/** @internal Clear expression cache (for testing) */
export function clearGuardExpressionCache(): void {
clearCoreCache();
guardDefMemory.clear();
}
// ── DB helpers ─────────────────────────────────────────────────
@@ -89,18 +84,11 @@ function listGuardDefs(db: Database): GuardProjectionDef[] {
initial_value: string;
sources: string;
}[];
const byName = new Map<string, GuardProjectionDef>();
for (const r of rows) {
byName.set(r.name, {
name: r.name,
initial_value: JSON.parse(r.initial_value) as unknown,
sources: JSON.parse(r.sources) as GuardSource[],
});
}
for (const [name, def] of guardDefMemory) {
byName.set(name, def);
}
return [...byName.values()];
return rows.map(r => ({
name: r.name,
initial_value: JSON.parse(r.initial_value) as unknown,
sources: JSON.parse(r.sources) as GuardSource[],
}));
}
function loadGuardRow(
@@ -133,7 +121,7 @@ export function registerGuard(db: Database, def: GuardProjectionDef): void {
JSON.stringify(def.sources),
now,
);
guardDefMemory.set(def.name, def);
}
/**
@@ -602,7 +602,7 @@ describe('createWorkflowRule', () => {
// TODO: abort-prevents-restart requires guard state replay on daemon restart
// Skipping for now — lifecycle detection uses event-based lastRole check
it.skip('abort prevents restart of same topic key', async () => {
it('abort prevents restart of same topic key', async () => {
setup();
let callCount = 0;
@@ -12,6 +12,7 @@
*/
import type { PulseStore } from '../store.js';
import { registerGuard, getGuardState } from '../guard-projection.js';
import {
END,
type ModeratorInput,
@@ -63,8 +64,17 @@ export function createWorkflowRule(
let prevSnapshotJson = '';
let checkpoint: WorkflowCheckpoint | null = null;
// Lifecycle check uses event-based lastRole detection (__end__, __abort__)
// No guard registration needed — avoids state replay issues on daemon restart
// Register lifecycle guard for state-machine protection
const db = store.getDatabase();
registerGuard(db, {
name: 'workflow-lifecycle',
initial_value: { status: 'unknown' },
sources: [
{ kind: '*.__start__', check: "state.status = 'unknown'", transition: "{'status':'active'}" },
{ kind: '*.__abort__', check: "state.status = 'active'", transition: "{'status':'aborted'}" },
{ kind: '*.__end__', check: "state.status = 'active'", transition: "{'status':'ended'}" },
],
});
return {
async tick(): Promise<WorkflowTickResult> {