Compare commits

..

1 Commits

Author SHA1 Message Date
operator
9195dc6bd1 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>
2026-04-19 12:24:21 +00:00

View File

@@ -19,7 +19,6 @@
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { CalendarBridgeClient } from './calendar-bridge';
import { ScheduleCache } from './schedule-cache';
import {
CalendarSlotResponse,
SlotStatus,
@@ -45,8 +44,6 @@ export interface CalendarSchedulerConfig {
};
/** Heartbeat interval in milliseconds (default: 60000) */
heartbeatIntervalMs?: number;
/** Schedule sync interval in milliseconds (default: 300000 = 5 min) */
syncIntervalMs?: number;
/** Enable verbose debug logging */
debug?: boolean;
/** Directory for state persistence (default: plugin data dir) */
@@ -98,10 +95,8 @@ interface SchedulerState {
currentSlot: CalendarSlotResponse | null;
/** Last heartbeat timestamp */
lastHeartbeatAt: Date | null;
/** Heartbeat interval handle */
/** Interval handle for cleanup */
intervalHandle: ReturnType<typeof setInterval> | null;
/** Schedule sync interval handle */
syncIntervalHandle: ReturnType<typeof setInterval> | null;
/** Set of slot IDs that have been deferred in current session */
deferredSlotIds: Set<string>;
/** Whether agent is currently processing a slot */
@@ -122,13 +117,10 @@ export class CalendarScheduler {
private config: Required<CalendarSchedulerConfig>;
private state: SchedulerState;
private stateFilePath: string;
/** Local cache of today's full schedule, synced periodically from backend */
private scheduleCache: ScheduleCache = new ScheduleCache();
constructor(config: CalendarSchedulerConfig) {
this.config = {
heartbeatIntervalMs: 60000, // 1 minute default
syncIntervalMs: 300_000, // 5 minutes default
debug: false,
stateDir: this.getDefaultStateDir(),
...config,
@@ -141,7 +133,6 @@ export class CalendarScheduler {
currentSlot: null,
lastHeartbeatAt: null,
intervalHandle: null,
syncIntervalHandle: null,
deferredSlotIds: new Set(),
isProcessing: false,
isRestartPending: false,
@@ -336,21 +327,14 @@ export class CalendarScheduler {
this.state.isRestartPending = false;
this.config.logger.info('Calendar scheduler started');
// Run initial sync + heartbeat immediately
this.runSync();
// Run initial heartbeat immediately
this.runHeartbeat();
// Schedule periodic heartbeats (slot execution checks)
// Schedule periodic heartbeats
this.state.intervalHandle = setInterval(
() => this.runHeartbeat(),
this.config.heartbeatIntervalMs
);
// Schedule periodic schedule sync (full day schedule refresh)
this.state.syncIntervalHandle = setInterval(
() => this.runSync(),
this.config.syncIntervalMs
);
}
/**
@@ -364,41 +348,10 @@ export class CalendarScheduler {
clearInterval(this.state.intervalHandle);
this.state.intervalHandle = null;
}
if (this.state.syncIntervalHandle) {
clearInterval(this.state.syncIntervalHandle);
this.state.syncIntervalHandle = null;
}
this.config.logger.info('Calendar scheduler stopped');
}
/**
* Sync today's full schedule from backend into local cache.
* Runs every syncIntervalMs (default: 5 min).
* Catches new slots assigned by other agents or the manager.
*/
async runSync(): Promise<void> {
if (!this.state.isRunning || this.state.isRestartPending) return;
const today = new Date().toISOString().slice(0, 10);
try {
const slots = await this.config.bridge.getDaySchedule(today);
if (slots) {
this.scheduleCache.sync(today, slots);
this.logDebug(`Schedule synced: ${slots.length} slots for ${today}`);
}
} catch (err) {
this.config.logger.warn(`Schedule sync failed: ${String(err)}`);
}
}
/**
* Get the local schedule cache (for status reporting / tools).
*/
getScheduleCache(): ScheduleCache {
return this.scheduleCache;
}
/**
* Execute a single heartbeat cycle.
* Fetches pending slots and handles execution logic.
@@ -658,11 +611,13 @@ Task Code: ${code}
Estimated Duration: ${duration} minutes
Slot Type: ${slot.slot_type}
Priority: ${slot.priority}
Working Sessions: ${jobData.working_sessions?.join(', ') || 'none recorded'}
Follow the daily-routine skill's task-handson workflow to execute this task.
Use harborforge_calendar_complete when finished, or harborforge_calendar_pause to pause.
Before going idle, check for overdue slots as described in the slot-complete workflow.`;
Please focus on this task for the allocated time. When you finish or need to pause,
report your progress back to the calendar system.
Working sessions: ${jobData.working_sessions?.join(', ') || 'none recorded'}
Start working on ${code} now.`;
}
/**
@@ -675,15 +630,19 @@ Before going idle, check for overdue slots as described in the slot-complete wor
switch (sysData.event) {
case 'ScheduleToday':
return `System Event: Schedule Today
Please review today's calendar and schedule any pending tasks or planning activities.
Estimated time: ${slot.estimated_duration} minutes.
Follow the daily-routine skill's plan-schedule workflow to plan today's work.`;
Check your calendar and plan the day's work.`;
case 'SummaryToday':
return `System Event: Daily Summary
Please provide a summary of today's activities and progress.
Estimated time: ${slot.estimated_duration} minutes.
Review today's completed, deferred, and abandoned slots. Write a summary to your daily note (memory/YYYY-MM-DD.md).`;
Review what was accomplished and prepare end-of-day notes.`;
case 'ScheduledGatewayRestart':
return `System Event: Scheduled Gateway Restart