fix(plugin): lift calendarScheduler to module scope (multi-register singleton)

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) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-21 10:54:36 +01:00
parent ff9929ad31
commit 428fc254ee
2 changed files with 33 additions and 10 deletions

View File

@@ -25,6 +25,25 @@ import {
CalendarScheduler, CalendarScheduler,
} from './calendar/index.js'; } 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 { interface PluginAPI {
logger: { logger: {
info: (...args: any[]) => void; info: (...args: any[]) => void;
@@ -108,7 +127,7 @@ function register(api: PluginAPI): void {
}, },
openclaw: { openclaw: {
version: api.runtime?.version || api.version || 'unknown', 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(), timestamp: new Date().toISOString(),
}; };
@@ -117,13 +136,9 @@ function register(api: PluginAPI): void {
// Periodic metadata push interval handle // Periodic metadata push interval handle
let metaPushInterval: ReturnType<typeof setInterval> | null = null; let metaPushInterval: ReturnType<typeof setInterval> | null = null;
// Calendar scheduler instance. // (calendarScheduler is module-scope — see top of file for the why.
// // Tools and lifecycle hooks all reference the same binding so the
// In multi-agent sync mode (the only path today) this is a // multi-register/single-start mismatch can't shadow them again.)
// {@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;
/** /**
* Push OpenClaw metadata to the Monitor bridge. * Push OpenClaw metadata to the Monitor bridge.
@@ -147,7 +162,7 @@ function register(api: PluginAPI): void {
const meta: OpenClawMeta = { const meta: OpenClawMeta = {
version: api.runtime?.version || api.version || 'unknown', version: api.runtime?.version || api.version || 'unknown',
plugin_version: '0.3.2', plugin_version: '0.3.3',
agents: agentNames.map(name => ({ name })), agents: agentNames.map(name => ({ name })),
}; };
@@ -291,8 +306,16 @@ function register(api: PluginAPI): void {
/** /**
* Initialize and start the calendar scheduler. * 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 { function startCalendarScheduler(): void {
if (calendarScheduler) {
logger.info('Calendar scheduler already started, skipping duplicate gateway_start');
return;
}
const live = resolveConfig(); const live = resolveConfig();
// Create bridge client (claw-instance level, not per-agent) // Create bridge client (claw-instance level, not per-agent)

View File

@@ -1,6 +1,6 @@
{ {
"name": "harbor-forge-plugin", "name": "harbor-forge-plugin",
"version": "0.3.2", "version": "0.3.3",
"description": "OpenClaw plugin for HarborForge monitor bridge and CLI integration", "description": "OpenClaw plugin for HarborForge monitor bridge and CLI integration",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",