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>
In multi-agent sync mode every harborforge_calendar_* tool was returning
`calendarScheduler.<method> is not a function`. The cause: index.ts replaced
`calendarScheduler` (typed `CalendarScheduler | null`) with a `{ stop() }`
stub right after wiring the runSync/runCheck intervals, so `isRunning()`,
`getCurrentSlot()`, `completeCurrentSlot()`, `abortCurrentSlot()`,
`pauseCurrentSlot()`, `resumeCurrentSlot()`, `getState()`,
`isRestartPending()` and `getStateFilePath()` all blew up at call time.
Replaces the stub with a `MultiAgentSchedulerHandle` that:
- tracks the last slot dispatched per agent (recorded by `wakeAgent`)
- exposes status/complete/abort/pause/resume taking the calling agentId
- resolves the implicit "current slot" via woken-cursor first then a
cache scan over not_started/deferred/ongoing slots
- PATCHes via `bridge.updateSlotAs(agentId, …)` so audit headers reflect
the real caller (bridge constructor agentId is 'unused' in multi-agent)
- mirrors the legacy `isRunning/isProcessing/getState/...` surface so
the single-agent fallback (`CalendarScheduler`) keeps working unchanged
Each calendar tool factory now takes `OpenClawPluginToolContext`, reads
`ctx.agentId`, and dispatches through the handle. Single-agent path
(when `calendarScheduler` is a real `CalendarScheduler`) is preserved
behind `instanceof` checks.
Drops the dead `trackSessionCompletion` poll loop (only definition, no
caller) which referenced the removed `completeCurrentSlot`. Bumps
plugin version 0.2.0 → 0.3.2.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues making HF→agent wakeup unusable in practice, surfaced by
DinD sim end-to-end test (recruiter agent + slot for 招募 manager task):
1. **Plugin re-woke the same slot every 30s.** The inline runCheck only
destructured agentId from scheduleCache.getAgentsWithDueSlots() and
dropped the slots array, then called wakeAgent without recording the
wake. The simplified inline scheduler also never PATCHes slot status
server-side from not_started→ongoing, so the next 30s check sees the
slot still due and wakes again. After 4 wakes the agent's wakeup
session was full of WAKEUP_OK noise.
Fix: keep slots in runCheck, add an in-memory wakedSlotKeys set
keyed by (agentId, slotId|virtual_id|scheduled_at). Dedupe on this
set; clear it inside the sync interval (fresh wake budget per sync).
Server-side slot transition still TODO (requires re-introducing the
CalendarScheduler class path or PATCH /calendar/slots/.../agent-update
here); the dedupe at least stops the wake spam.
2. **Wakeup message had no slot context.** The wakeup body just said
'follow hf-wakeup workflow' with no slot id/event_data/task_code.
The agent then had to call harborforge_calendar_status to learn
anything — which itself is broken in the simplified scheduler (it
queries a CalendarScheduler instance that never gets created).
Fix: pass dueSlots into wakeAgent and inline the highest-priority
slot's {slot_id, scheduled_at, priority, slot_type, event_data} as
a JSON block in the wakeup message. The agent reads event_data.
task_code directly and routes via workflow_lookup without any
round-trip. Per PLG-CAL-001 docs in hf-hangman-lab SKILL.md, this
is the documented contract; we are bringing the message in line.
3. **contracts.tools listed 5 of the 9 registered tools.** Manifest had
harborforge_status/telemetry/monitor_telemetry/calendar_status/
calendar_complete. Code also registers calendar_abort, calendar_pause,
calendar_resume, harborforge_restart_status. With the new OpenClaw
plugin host enforcement (same gotcha that bit Meridian — see
zhi/Meridian#2), undeclared tools are silently dropped from the
agent's tool list, so abort/pause/resume cannot be called by the
agent. plugin doctor was emitting:
'plugin tool is undeclared (harbor-forge): harborforge_calendar_abort'
for each missing tool.
Fix: add the 4 missing tool names to contracts.tools.
Also use api.config as the primary config source in wakeAgent (current
public API), falling back to runtime.config.loadConfig() for older
hosts — same pattern as the Meridian fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ESM conversion:
- plugin/package.json: add "type": "module".
- plugin/tsconfig.json: switch module/moduleResolution to "nodenext"; bump
target to ES2022.
- All relative imports across plugin/ now carry .js extensions as required
by Node ESM (nodenext module resolution).
- plugin/index.ts: replace `require('./calendar/schedule-cache')` with a
proper top-level import; switch `from 'os'` to `from 'node:os'`.
Plugin SDK convention update:
- Wrap default export with definePluginEntry({ id, name, description,
register }) per the current openclaw plugin authoring contract.
- Modernize plugin/openclaw.plugin.json: drop entry/version, add
activation.onStartup so gateway_start fires for this plugin at boot,
declare contracts.tools listing the five harborforge_* tools.
- Add a local plugin/openclaw-sdk.d.ts with ambient declarations for the
focused subpaths (openclaw/plugin-sdk/plugin-entry,
openclaw/plugin-sdk/core). We deliberately do NOT add openclaw as an
npm devDependency: the installer's `npm install --omit=dev` step trips
over openclaw's own (deeply nested) dependency graph when listed via
file:.../openclaw, and the runtime contract is provided by the gateway
loader anyway.
- The local PluginAPI interface is preserved (broader than the standard
OpenClawPluginApi — it surfaces `version`/`runtime`/`spawn`); the
register function is cast at the definePluginEntry boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. ScheduleCache: local cache of today's schedule, synced every 5 min
from HF backend via new getDaySchedule() API
2. Wakeup prompts updated to reference daily-routine skill workflows
(task-handson, plan-schedule, slot-complete)
3. Agent wakeup via dispatchInboundMessageWithDispatcher (in-process)
- Same mechanism as Discord plugin
- Creates unique session per slot: agent:{agentId}:hf-calendar:slot-{slotId}
- No WebSocket, CLI, or cron dependency
- Verified working on test environment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use api.runtime.version for openclaw version and
api.runtime.config.loadConfig() for agent list. Eliminates the
periodic openclaw agents list subprocess that caused high CPU usage.
api.version returns plugin API version (0.2.0), not the openclaw release
version. Use OPENCLAW_SERVICE_VERSION env var set by the gateway instead.
Also increase listOpenClawAgents timeout from 15s to 30s since plugin
loading takes ~16s on T2.
- Add state persistence (persistState/restoreState) for recovery after restart
- Add handleScheduledGatewayRestart method that:
- Persists current scheduler state to disk
- Sends final heartbeat to backend before shutdown
- Stops the calendar scheduler (pauses scheduled tasks)
- Add isRestartPending flag to prevent new slot processing during restart
- Add isScheduledGatewayRestart helper to detect restart events
- Update scheduler to detect and handle ScheduledGatewayRestart events
- Add new tools: harborforge_restart_status, harborforge_calendar_pause/resume
- Export isRestartPending and getStateFilePath methods
- Bump plugin version to 0.3.1
- Add CalendarScheduler class to manage periodic heartbeat and slot execution
- Implement agent wakeup logic when Idle and slots are pending
- Handle slot status transitions (attended, ongoing, deferred)
- Support both real and virtual slot materialization
- Add task context building for different event types (job, system, entertainment)
- Integrate scheduler into main plugin index.ts
- Add new plugin tools: harborforge_calendar_status, complete, abort
- MonitorBridgeClient gains pushOpenClawMeta() method for POST /openclaw
- OpenClawMeta interface defines version/plugin_version/agents payload
- Plugin pushes metadata on gateway_start (delayed 2s) and periodically
- Interval aligns with reportIntervalSec (default 30s)
- Pushes are non-fatal — plugin continues if Monitor is unreachable
- Interval cleanup on gateway_stop
- Updated monitor-server-connector-plan.md with new architecture
Major changes:
- Renamed plugin id from harborforge-monitor to harbor-forge (TODO 4.1)
- Removed sidecar server/ directory and spawn logic (TODO 4.2)
- Added monitorPort to plugin config schema (TODO 4.3)
- Added --install-cli flag to installer for building hf CLI (TODO 4.4)
- skills/hf/ only deployed when --install-cli is present (TODO 4.5)
- Plugin now serves telemetry data directly via tools
- Installer handles migration from old plugin name
- Bumped version to 0.2.0
- Report remote OpenClaw CLI version as openclaw_version
- Report harborforge-monitor plugin version as plugin_version
- Pass plugin version from plugin runtime to sidecar
- Read live config via api.pluginConfig/api.config helper
- Mirror dirigent plugin registration pattern
- Read base config from api.pluginConfig
- Read live config from api.config.plugins.entries.harborforge-monitor.config
- Support gateway_start/gateway_stop lifecycle only
- Compile nested plugin/core files