# 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`: ```typescript class ScheduleCache { private slots: Map; // slotId → slot private lastSyncAt: Date | null; async sync(bridge: CalendarBridgeClient): Promise; // fetch today's full schedule getDueSlots(now: Date): CalendarSlotResponse[]; // scheduled_at <= now && NOT_STARTED/DEFERRED updateSlot(id: string, update: Partial): void; // local update getAll(): CalendarSlotResponse[]; } ``` ### 2. Add CalendarBridgeClient.getDaySchedule() New endpoint call: ```typescript async getDaySchedule(date: string): Promise // 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: ```typescript // 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