OpenClaw's Codex tool dispatcher (thread-lifecycle:255) expects every
tool execute() to return { content: [...] } and calls result.content.reduce()
to compute total text length. All 9 harborforge_* tools returned bare
objects ({ running, processing, currentSlot, ... }) which has no
.content field — so .reduce of undefined threw, and the agent saw the
cryptic 'Cannot read properties of undefined (reading reduce)' on
every call. This silently blocked every calendar slot transition on
prod for hours: agents could call harborforge_calendar_complete but
it always errored, so slots never moved out of not_started.
Fix is at the registerTool boundary: api.registerTool is wrapped once
to coerce every tool's execute return through ensureMcpContentShape.
Tools that already return the correct shape are unchanged. No per-tool
edits needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
E2e showed the old wakeup text trapped agents in an ack-only loop:
> "You have due slots. Follow the `hf-wakeup` workflow of skill
> `hf-hangman-lab` to proceed. Only reply `WAKEUP_OK` in this session."
The two clauses contradicted each other — "follow the workflow" vs
"only reply WAKEUP_OK". MiniMax-M2.5 prioritised the literal "only"
and never proceeded past the ack; the scheduler then re-woke every 30s
because the slot stayed `not_started`, and the agent kept re-acking
forever (verified: 3 consecutive WAKEUP_OK-only replies across slot 7).
Rewrites the wakeup message to be explicit:
- first line MUST be `WAKEUP_OK` (the ack token the plugin looks for)
- then continue IN THE SAME session: drive calendar_status → task
fetch → sub-workflow → calendar_complete/abort
- flags the loop trap so the agent knows what to avoid
Bumps version 0.3.3 → 0.3.4.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>