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>
This commit is contained in:
operator
2026-04-18 17:45:32 +00:00
parent 9af8204376
commit 4a66a7cc90
5 changed files with 298 additions and 3 deletions

112
REFACTOR_PLAN.md Normal file
View File

@@ -0,0 +1,112 @@
# 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<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:
```typescript
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:
```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