fix(council-v2): serialize topicTicker to prevent fire-and-forget concurrency

executorLoop fire-and-forgets each effect execution. Multiple concurrent
execute() calls each invoke topicTicker(), causing concurrent tick() calls
that bypass Moore diff (both read same prevSnapshotJson before either updates).

Fix: chain tick() calls via promise serialization. Critical section
(read → diff → update baseline) is now guaranteed sequential.
This commit is contained in:
2026-04-17 03:02:51 +00:00
parent 4caac0b68c
commit 95d2e0679e
+12 -4
View File
@@ -22,9 +22,17 @@ import type { TopicRule } from './topic-rule-adapter.js';
* suitable for calling at the end of a runPulse tick cycle.
*/
export function createTopicTicker(rules: TopicRule[]): () => Promise<void> {
return async () => {
for (const rule of rules) {
await rule.tick();
}
let pending: Promise<void> | null = null;
return () => {
// Serialize tick calls: if a tick is in progress, chain after it.
// This ensures the Moore machine's critical section (read → diff → update baseline)
// is never concurrent, even when called from fire-and-forget executors.
const run = async () => {
for (const rule of rules) {
await rule.tick();
}
};
pending = (pending ?? Promise.resolve()).then(run, run);
return pending;
};
}