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:
112
REFACTOR_PLAN.md
Normal file
112
REFACTOR_PLAN.md
Normal 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
|
||||
Reference in New Issue
Block a user