From 428fc254ee7cb99fd537169a07e107603088407b Mon Sep 17 00:00:00 2001 From: hzhang Date: Thu, 21 May 2026 10:54:36 +0100 Subject: [PATCH] fix(plugin): lift calendarScheduler to module scope (multi-register singleton) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trying the prior multi-agent-handle fix in dind-t2 surfaced a second bug that PR #7 didn't reach: `harborforge_calendar_status` still returned `Calendar scheduler not running` even though the gateway log showed the scheduler had started 30+ seconds before the agent's call. ## Root cause `register()` is invoked once per agent — `grep -c "HarborForge plugin registered" /tmp/gw-stdout.log` reports 5 for a 5-agent claw. Every invocation creates its own `let calendarScheduler` closure binding. But `gateway_start` fires once and we only call `startCalendarScheduler()` through that single hook, so exactly one of the five closures sees the handle and the other four keep their bindings at `null`. The host's tool router picks one of the five duplicate `harborforge_calendar_status` registrations to dispatch to — most of the time it's one of the four "null" closures, which is why every wakeup the agent saw `Calendar scheduler not running`. ## Fix Lift `let calendarScheduler` out of `register()` and into module scope. All five register-call closures now reference the same binding; once the single `gateway_start` initialises it, every tool sees it. `startCalendarScheduler()` now early-returns when `calendarScheduler` is already set, so duplicate `gateway_start` firings (if the host ever does that) don't double-install intervals. Bumps version 0.3.2 → 0.3.3. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin/index.ts | 41 ++++++++++++++++++++++++++++++++--------- plugin/package.json | 2 +- 2 files changed, 33 insertions(+), 10 deletions(-) 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",