diff --git a/plugin/index.ts b/plugin/index.ts index 253acb0..65b5c12 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -25,6 +25,25 @@ import { CalendarScheduler, } from './calendar/index.js'; +// --------------------------------------------------------------------------- +// Module-scope calendar scheduler singleton. +// +// `register()` is called multiple times per gateway boot — once per agent +// (we see 5 `HarborForge plugin registered` lines for 5 agents on dind-t2). +// `gateway_start` only fires once, so before this lift the +// `startCalendarScheduler()` setup ran inside ONE closure while four other +// closures kept their own `calendarScheduler = null`. Whichever of the five +// tool registrations the gateway picked at call time was effectively a coin +// flip, and four times out of five `harborforge_calendar_status` returned +// `Calendar scheduler not running` even though the scheduler was active. +// +// Keeping the singleton at module scope removes the per-`register()` shadow: +// the scheduler is started once, every closure reads the same binding, and +// `startCalendarScheduler()` is idempotent so duplicate `gateway_start` +// firings are harmless. +// --------------------------------------------------------------------------- +let calendarScheduler: MultiAgentSchedulerHandle | CalendarScheduler | null = null; + interface PluginAPI { logger: { info: (...args: any[]) => void; @@ -108,7 +127,7 @@ function register(api: PluginAPI): void { }, openclaw: { version: api.runtime?.version || api.version || 'unknown', - pluginVersion: '0.3.2', // Bumped for PLG-CAL-004 + pluginVersion: '0.3.3', // Bumped for PLG-CAL-004 }, timestamp: new Date().toISOString(), }; @@ -117,13 +136,9 @@ function register(api: PluginAPI): void { // Periodic metadata push interval handle let metaPushInterval: ReturnType | null = null; - // Calendar scheduler instance. - // - // In multi-agent sync mode (the only path today) this is a - // {@link MultiAgentSchedulerHandle}. The legacy `CalendarScheduler` type - // is retained in the union for compatibility with the typed-only single- - // agent path that may be reintroduced later. - let calendarScheduler: MultiAgentSchedulerHandle | CalendarScheduler | null = null; + // (calendarScheduler is module-scope — see top of file for the why. + // Tools and lifecycle hooks all reference the same binding so the + // multi-register/single-start mismatch can't shadow them again.) /** * Push OpenClaw metadata to the Monitor bridge. @@ -147,7 +162,7 @@ function register(api: PluginAPI): void { const meta: OpenClawMeta = { version: api.runtime?.version || api.version || 'unknown', - plugin_version: '0.3.2', + plugin_version: '0.3.3', agents: agentNames.map(name => ({ name })), }; @@ -291,8 +306,16 @@ function register(api: PluginAPI): void { /** * Initialize and start the calendar scheduler. + * + * Idempotent — `gateway_start` may fire once per `register()` invocation + * (the host calls `register` per agent), and we only want one set of + * sync/check intervals across the whole process. */ function startCalendarScheduler(): void { + if (calendarScheduler) { + logger.info('Calendar scheduler already started, skipping duplicate gateway_start'); + return; + } const live = resolveConfig(); // Create bridge client (claw-instance level, not per-agent) diff --git a/plugin/package.json b/plugin/package.json index cc77f67..afd2b44 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,6 +1,6 @@ { "name": "harbor-forge-plugin", - "version": "0.3.2", + "version": "0.3.3", "description": "OpenClaw plugin for HarborForge monitor bridge and CLI integration", "type": "module", "main": "dist/index.js",