feat: guard defs table-only, lifecycle guard restored, daemon fault-tolerant
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user