diff --git a/plugin/index.ts b/plugin/index.ts index 47481ca..1510d05 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -197,7 +197,18 @@ function register(api: PluginAPI): void { * Wake agent via dispatchInboundMessage — same mechanism used by Discord plugin. * Direct in-process call, no WebSocket or CLI needed. */ - async function wakeAgent(agentId: string): Promise { + async function wakeAgent( + agentId: string, + dueSlots?: Array<{ + id?: number | null; + virtual_id?: string | null; + event_data?: any; + scheduled_at?: string; + priority?: number; + slot_type?: string; + [k: string]: unknown; + }> + ): Promise { logger.info(`Waking agent ${agentId}: has due slots`); const sessionKey = `agent:${agentId}:hf-wakeup`; @@ -208,13 +219,39 @@ function register(api: PluginAPI): void { /* webpackIgnore: true */ sdkPath ); - const cfg = api.runtime?.config?.loadConfig?.(); + // api.config first (current public API). Fall back to deprecated + // runtime.config.loadConfig() for older host versions. Both should + // contain agents.list / channels for dispatch routing. + const cfg = (api as any).config ?? api.runtime?.config?.loadConfig?.(); if (!cfg) { logger.error('Cannot load OpenClaw config for dispatch'); return false; } - const wakeupMessage = `You have due slots. Follow the \`hf-wakeup\` workflow of skill \`hf-hangman-lab\` to proceed. Only reply \`WAKEUP_OK\` in this session.`; + // Inline the highest-priority due slot's context so the agent does + // not need a second round-trip to harborforge_calendar_status. The + // agent can read event_data.task_code / task_title etc. directly. + let slotBlock = ''; + const top = dueSlots && dueSlots.length ? dueSlots[0] : undefined; + if (top) { + slotBlock = `\n\nMatching slot:\n\`\`\`json\n${JSON.stringify( + { + slot_id: top.id ?? null, + virtual_id: top.virtual_id ?? null, + scheduled_at: top.scheduled_at ?? null, + priority: top.priority ?? null, + slot_type: top.slot_type ?? null, + event_data: top.event_data ?? null, + }, + null, + 2 + )}\n\`\`\``; + } + + const wakeupMessage = + `You have due slots. Follow the \`hf-wakeup\` workflow of skill ` + + `\`hf-hangman-lab\` to proceed. Only reply \`WAKEUP_OK\` in this ` + + `session.${slotBlock}`; const result = await dispatchInboundMessageWithDispatcher({ ctx: { @@ -308,28 +345,59 @@ function register(api: PluginAPI): void { } } + // Track wakes already dispatched for a slot in the current sync + // window — the simplified inline scheduler does not PATCH slot + // status server-side, so without dedupe the check loop re-wakes + // the same slot every 30s. Set is cleared by runSync (fresh wake + // budget per sync). + const wakedSlotKeys = new Set(); + // Check: find agents with due slots and wake them async function runCheck() { const now = new Date(); const agentsWithDue = scheduleCache.getAgentsWithDueSlots(now); - for (const { agentId } of agentsWithDue) { - // Check if agent is busy - const status = await calendarBridge.getAgentStatus(agentId); + for (const { agentId, slots } of agentsWithDue) { + // Filter out slots we've already woken this sync window + const fresh = slots.filter((s) => { + const key = `${agentId}::${s.id ?? s.virtual_id ?? s.scheduled_at}`; + if (wakedSlotKeys.has(key)) return false; + return true; + }); + if (fresh.length === 0) continue; + + // Check if agent is busy (best effort; backend may 405 the GET + // — treat unknown as not-busy so wakeup still fires) + let status: string | null = null; + try { + status = await calendarBridge.getAgentStatus(agentId); + } catch { + status = null; + } if (status === 'busy' || status === 'offline' || status === 'exhausted') { continue; } - // Wake the agent - await wakeAgent(agentId); + // Wake the agent with the slot context inlined + const ok = await wakeAgent(agentId, fresh); + if (ok) { + for (const s of fresh) { + const key = `${agentId}::${s.id ?? s.virtual_id ?? s.scheduled_at}`; + wakedSlotKeys.add(key); + } + } } } - // Initial sync - runSync(); + // Initial sync (also resets the wake-dedupe window) + const runSyncReset = async () => { + wakedSlotKeys.clear(); + await runSync(); + }; + runSyncReset(); // Start intervals - const syncHandle = setInterval(runSync, SYNC_INTERVAL_MS); + const syncHandle = setInterval(runSyncReset, SYNC_INTERVAL_MS); const checkHandle = setInterval(runCheck, CHECK_INTERVAL_MS); // Store handles for cleanup (reuse calendarScheduler variable) diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index 9acecef..d49cd37 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -11,7 +11,11 @@ "harborforge_telemetry", "harborforge_monitor_telemetry", "harborforge_calendar_status", - "harborforge_calendar_complete" + "harborforge_calendar_complete", + "harborforge_calendar_abort", + "harborforge_calendar_pause", + "harborforge_calendar_resume", + "harborforge_restart_status" ] }, "configSchema": { diff --git a/plugin/package-lock.json b/plugin/package-lock.json index d57e993..80d8083 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -9,14 +9,14 @@ "version": "0.2.0", "license": "MIT", "devDependencies": { - "@types/node": "^20.0.0", + "@types/node": "^20.19.41", "typescript": "^5.0.0" } }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/plugin/package.json b/plugin/package.json index 37a353f..eb3d4fb 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -10,7 +10,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@types/node": "^20.0.0", + "@types/node": "^20.19.41", "typescript": "^5.0.0" }, "license": "MIT"