feat: schedule cache, workflow-aligned prompts, dispatchInbound wakeup

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>
This commit is contained in:
operator
2026-04-19 08:10:39 +00:00
parent 248adfaafd
commit 809eb13fa8
6 changed files with 338 additions and 53 deletions

View File

@@ -188,56 +188,52 @@ export default {
}
/**
* Wake/spawn agent with task context for slot execution.
* This is the callback invoked by CalendarScheduler when a slot is ready.
* Wake agent via dispatchInboundMessage — same mechanism used by Discord plugin.
* Direct in-process call, no WebSocket or CLI needed.
*/
async function wakeAgent(context: AgentWakeContext): Promise<boolean> {
logger.info(`Waking agent for slot: ${context.taskDescription}`);
const agentId = process.env.AGENT_ID || 'unknown';
const slotId = context.slot.id ?? context.slot.virtual_id ?? 'unknown';
const sessionKey = `agent:${agentId}:hf-calendar:slot-${slotId}`;
try {
// Method 1: Use OpenClaw spawn API if available (preferred)
if (api.spawn) {
const result = await api.spawn({
task: context.prompt,
timeoutSeconds: context.slot.estimated_duration * 60, // Convert to seconds
});
// Dynamic import — resolved at runtime by OpenClaw's module system
const sdkPath = 'openclaw/plugin-sdk/reply-runtime';
const { dispatchInboundMessageWithDispatcher } = await import(
/* webpackIgnore: true */ sdkPath
);
if (result?.sessionId) {
logger.info(`Agent spawned for calendar slot: session=${result.sessionId}`);
// Track session completion
trackSessionCompletion(result.sessionId, context);
return true;
}
const cfg = api.runtime?.config?.loadConfig?.();
if (!cfg) {
logger.error('Cannot load OpenClaw config for dispatch');
return false;
}
// Method 2: Send notification/alert to wake agent (fallback)
// This relies on the agent's heartbeat to check for notifications
logger.warn('OpenClaw spawn API not available, using notification fallback');
// Send calendar wakeup notification via backend
const live = resolveConfig();
const agentId = process.env.AGENT_ID || 'unknown';
const notifyResponse = await fetch(`${live.backendUrl}/calendar/agent/notify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Agent-ID': agentId,
'X-Claw-Identifier': live.identifier || hostname(),
const result = await dispatchInboundMessageWithDispatcher({
ctx: {
Body: context.prompt,
SessionKey: sessionKey,
From: 'harborforge-calendar',
Provider: 'harborforge',
},
cfg,
dispatcherOptions: {
deliver: async (payload: any) => {
// No delivery — agent works silently
logger.info(`Agent reply for slot ${slotId}: ${(payload.text || '').slice(0, 100)}`);
},
},
body: JSON.stringify({
agent_id: agentId,
message: context.prompt,
slot_id: context.slot.id || context.slot.virtual_id,
task_description: context.taskDescription,
}),
});
return notifyResponse.ok;
logger.info(`Agent dispatched for slot ${slotId}: ${result?.status || 'ok'}`);
return true;
} catch (err) {
logger.error('Failed to wake agent:', err);
} catch (err: any) {
const msg = err?.message || err?.code || String(err);
const stack = err?.stack?.split('\n').slice(0, 3).join(' | ') || '';
logger.error(`Failed to dispatch agent for slot: ${msg} ${stack}`);
return false;
}
}