|
|
|
@@ -25,25 +25,6 @@ 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;
|
|
|
|
@@ -73,30 +54,6 @@ interface PluginAPI {
|
|
|
|
getAgentStatus?: () => Promise<{ status: string } | null>;
|
|
|
|
getAgentStatus?: () => Promise<{ status: string } | null>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Coerce a tool execute() return value into the MCP `{ content: [...] }`
|
|
|
|
|
|
|
|
* shape that the openclaw Codex tool dispatcher requires.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Background: openclaw's `convertToolContents()` does `result.content.reduce(...)`
|
|
|
|
|
|
|
|
* to compute total text length before flattening. Every HF tool here returned a
|
|
|
|
|
|
|
|
* bare object (`{ running, processing, currentSlot, ... }`) which has no
|
|
|
|
|
|
|
|
* `.content` field, so `undefined.reduce` threw and every call to
|
|
|
|
|
|
|
|
* `harborforge_*` from a Codex-harness agent surfaced as the cryptic
|
|
|
|
|
|
|
|
* `Cannot read properties of undefined (reading 'reduce')`. The fix is to
|
|
|
|
|
|
|
|
* wrap every tool's execute return; doing it at the `registerTool` boundary
|
|
|
|
|
|
|
|
* keeps each tool body unchanged.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function ensureMcpContentShape(result: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
result && typeof result === 'object' &&
|
|
|
|
|
|
|
|
Array.isArray((result as { content?: unknown }).content)
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
return result as { content: Array<{ type: 'text'; text: string }> };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
|
|
|
|
|
|
return { content: [{ type: 'text', text }] };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function register(api: PluginAPI): void {
|
|
|
|
function register(api: PluginAPI): void {
|
|
|
|
const logger = api.logger || {
|
|
|
|
const logger = api.logger || {
|
|
|
|
info: (...args: any[]) => console.log('[HarborForge]', ...args),
|
|
|
|
info: (...args: any[]) => console.log('[HarborForge]', ...args),
|
|
|
|
@@ -105,22 +62,6 @@ function register(api: PluginAPI): void {
|
|
|
|
warn: (...args: any[]) => console.warn('[HarborForge]', ...args),
|
|
|
|
warn: (...args: any[]) => console.warn('[HarborForge]', ...args),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Wrap api.registerTool so every tool's execute() return is coerced into
|
|
|
|
|
|
|
|
// the MCP `{ content: [...] }` shape openclaw expects. See
|
|
|
|
|
|
|
|
// `ensureMcpContentShape` above.
|
|
|
|
|
|
|
|
const _origRegisterTool = api.registerTool.bind(api);
|
|
|
|
|
|
|
|
api.registerTool = (factory: (ctx: any) => any) => {
|
|
|
|
|
|
|
|
_origRegisterTool((ctx: any) => {
|
|
|
|
|
|
|
|
const def = factory(ctx);
|
|
|
|
|
|
|
|
if (!def || typeof def.execute !== 'function') return def;
|
|
|
|
|
|
|
|
const origExecute = def.execute;
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
...def,
|
|
|
|
|
|
|
|
execute: async (...args: any[]) => ensureMcpContentShape(await origExecute(...args)),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resolveConfig() {
|
|
|
|
function resolveConfig() {
|
|
|
|
return getPluginConfig(api);
|
|
|
|
return getPluginConfig(api);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -167,7 +108,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.4', // Bumped for PLG-CAL-004
|
|
|
|
pluginVersion: '0.3.2', // Bumped for PLG-CAL-004
|
|
|
|
},
|
|
|
|
},
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@@ -176,9 +117,13 @@ 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;
|
|
|
|
|
|
|
|
|
|
|
|
// (calendarScheduler is module-scope — see top of file for the why.
|
|
|
|
// Calendar scheduler instance.
|
|
|
|
// Tools and lifecycle hooks all reference the same binding so the
|
|
|
|
//
|
|
|
|
// multi-register/single-start mismatch can't shadow them again.)
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Push OpenClaw metadata to the Monitor bridge.
|
|
|
|
* Push OpenClaw metadata to the Monitor bridge.
|
|
|
|
@@ -202,7 +147,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.4',
|
|
|
|
plugin_version: '0.3.2',
|
|
|
|
agents: agentNames.map(name => ({ name })),
|
|
|
|
agents: agentNames.map(name => ({ name })),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@@ -307,21 +252,10 @@ function register(api: PluginAPI): void {
|
|
|
|
)}\n\`\`\``;
|
|
|
|
)}\n\`\`\``;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// First-line ack `WAKEUP_OK` is the plugin's ack-receipt token; the
|
|
|
|
|
|
|
|
// agent MUST then continue in the same session and drive the
|
|
|
|
|
|
|
|
// `hf-wakeup` workflow to completion (calendar_status → task fetch →
|
|
|
|
|
|
|
|
// sub-workflow → calendar_complete/abort). Without that continuation
|
|
|
|
|
|
|
|
// the scheduler keeps re-waking every 30s because the slot stays
|
|
|
|
|
|
|
|
// `not_started` forever.
|
|
|
|
|
|
|
|
const wakeupMessage =
|
|
|
|
const wakeupMessage =
|
|
|
|
`You have due slots. **First line of your reply MUST be exactly ` +
|
|
|
|
`You have due slots. Follow the \`hf-wakeup\` workflow of skill ` +
|
|
|
|
`\`WAKEUP_OK\`** so the plugin records the ack. Then, **in this ` +
|
|
|
|
`\`hf-hangman-lab\` to proceed. Only reply \`WAKEUP_OK\` in this ` +
|
|
|
|
`same session**, drive the \`hf-wakeup\` workflow of skill ` +
|
|
|
|
`session.${slotBlock}`;
|
|
|
|
`\`hf-hangman-lab\` to completion — read slot context, call the ` +
|
|
|
|
|
|
|
|
`harborforge_calendar_* tools, route to the right sub-workflow, ` +
|
|
|
|
|
|
|
|
`and finish with harborforge_calendar_complete or abort. Do NOT ` +
|
|
|
|
|
|
|
|
`stop after the ack — the scheduler will re-wake you every 30s ` +
|
|
|
|
|
|
|
|
`until the slot transitions out of \`not_started\`.${slotBlock}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const result = await dispatchInboundMessageWithDispatcher({
|
|
|
|
const result = await dispatchInboundMessageWithDispatcher({
|
|
|
|
ctx: {
|
|
|
|
ctx: {
|
|
|
|
@@ -357,16 +291,8 @@ 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)
|
|
|
|
|