Files
HarborForge.OpenclawPlugin/REFACTOR_PLAN.md
operator 7e4750fcc4 feat: add local schedule cache + periodic sync to CalendarScheduler
- New ScheduleCache class: maintains today's full schedule locally
- CalendarBridgeClient.getDaySchedule(): fetch all slots for a date
- Scheduler now runs two intervals:
  - Heartbeat (60s): existing slot execution flow (unchanged)
  - Sync (5min): pulls full day schedule into local cache
- Exposes getScheduleCache() for tools and status reporting

This enables the plugin to detect slots assigned by other agents
between heartbeats and provides a complete local view of the schedule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 17:45:32 +00:00

3.9 KiB

CalendarScheduler Refactor Plan

Current Design

Every 60s:
  heartbeat() → POST /calendar/agent/heartbeat → returns pending slots
  if idle → select highest priority → executeSlot → wakeAgent(spawn)
  if busy → defer all pending slots

Problems:

  1. Every heartbeat queries backend for pending slots — no local awareness of full schedule
  2. Cannot detect slots assigned by other agents between heartbeats
  3. 60s interval is too frequent for sync but too infrequent for precise wakeup
  4. Wakeup via api.spawn() creates a plain session, not a Discord private channel

Target Design

Every 5-10 min (sync interval):
  syncSchedule() → GET /calendar/day → update local today cache

Every 30s (check interval):
  checkDueSlots() → scan local cache for due slots
  if due slot found:
    confirmAgentStatus() → GET /calendar/agent/status
    if not busy → wakeAgent (via Dirigent moderator bot private channel)

Changes Required

1. Add Local Schedule Cache

New class ScheduleCache:

class ScheduleCache {
  private slots: Map<string, CalendarSlotResponse>;  // slotId → slot
  private lastSyncAt: Date | null;

  async sync(bridge: CalendarBridgeClient): Promise<void>;  // fetch today's full schedule
  getDueSlots(now: Date): CalendarSlotResponse[];  // scheduled_at <= now && NOT_STARTED/DEFERRED
  updateSlot(id: string, update: Partial<CalendarSlotResponse>): void;  // local update
  getAll(): CalendarSlotResponse[];
}

2. Add CalendarBridgeClient.getDaySchedule()

New endpoint call:

async getDaySchedule(date: string): Promise<CalendarSlotResponse[]>
// GET /calendar/day?date=YYYY-MM-DD

This fetches ALL slots for the day, not just pending ones. The existing heartbeat() only returns NOT_STARTED/DEFERRED.

3. Split Heartbeat into Sync + Check

Replace single runHeartbeat() with two intervals:

// Sync: every 5 min — pull full schedule from backend
this.syncInterval = setInterval(() => this.runSync(), 300_000);

// Check: every 30s — scan local cache for due slots
this.checkInterval = setInterval(() => this.runCheck(), 30_000);

runSync():

  1. bridge.getDaySchedule(today) → update cache
  2. Still send heartbeat to keep backend informed of agent liveness

runCheck():

  1. cache.getDueSlots(now) → find due slots
  2. Filter out session-deferred slots
  3. If agent idle → select highest priority → execute

4. Wakeup via Dirigent (future)

Change wakeAgent() to create a private Discord channel via Dirigent moderator bot instead of api.spawn(). This requires:

  • Access to Dirigent's moderator bot token or cross-plugin API
  • Creating a private channel with only the target agent
  • Posting the wakeup prompt as a message

For now: Keep api.spawn() as the wakeup method. The Dirigent integration can be added later as it requires cross-plugin coordination.

Implementation Order

  1. Add ScheduleCache class (new file: plugin/calendar/schedule-cache.ts)
  2. Add getDaySchedule() to CalendarBridgeClient
  3. Refactor CalendarScheduler:
    • Replace single interval with sync + check intervals
    • Use cache instead of heartbeat for slot discovery
    • Keep heartbeat for agent liveness reporting (reduced frequency)
  4. Update state persistence for new structure
  5. Keep existing wakeAgent/completion/abort/pause/resume tools unchanged

Files to Modify

File Changes
plugin/calendar/schedule-cache.ts New file
plugin/calendar/calendar-bridge.ts Add getDaySchedule()
plugin/calendar/scheduler.ts Refactor heartbeat → sync + check
plugin/calendar/index.ts Export new types

Risk Assessment

  • Low risk: ScheduleCache is additive, doesn't break existing behavior
  • Medium risk: Splitting heartbeat changes core scheduling logic
  • Mitigation: Keep heartbeat() method intact, use it for liveness reporting alongside new sync