diff --git a/packages/pulse/src/bin/workflow-daemon.ts b/packages/pulse/src/bin/workflow-daemon.ts index 3adfb04..cf0b111 100644 --- a/packages/pulse/src/bin/workflow-daemon.ts +++ b/packages/pulse/src/bin/workflow-daemon.ts @@ -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); diff --git a/packages/pulse/src/guard-projection.ts b/packages/pulse/src/guard-projection.ts index d455cfc..78d8cb0 100644 --- a/packages/pulse/src/guard-projection.ts +++ b/packages/pulse/src/guard-projection.ts @@ -46,14 +46,9 @@ export function initGuardSchema(db: Database): void { db.exec(GUARD_SCHEMA); } -// ── Memory cache ─────────────────────────────────────────────── - -const guardDefMemory = new Map(); - -/** @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(); - 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); + } /** diff --git a/packages/pulse/src/workflows/workflow-rule-adapter.test.ts b/packages/pulse/src/workflows/workflow-rule-adapter.test.ts index f6bf062..775e930 100644 --- a/packages/pulse/src/workflows/workflow-rule-adapter.test.ts +++ b/packages/pulse/src/workflows/workflow-rule-adapter.test.ts @@ -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; diff --git a/packages/pulse/src/workflows/workflow-rule-adapter.ts b/packages/pulse/src/workflows/workflow-rule-adapter.ts index b19bc10..9c5819a 100644 --- a/packages/pulse/src/workflows/workflow-rule-adapter.ts +++ b/packages/pulse/src/workflows/workflow-rule-adapter.ts @@ -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 {