- 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>
110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
/**
|
|
* Local cache of today's calendar schedule.
|
|
* Synced periodically from HF backend, checked locally for due slots.
|
|
*/
|
|
|
|
import type { CalendarSlotResponse } from "./types.js";
|
|
|
|
export class ScheduleCache {
|
|
private slots: Map<string, CalendarSlotResponse> = new Map();
|
|
private lastSyncAt: Date | null = null;
|
|
private cachedDate: string | null = null; // YYYY-MM-DD
|
|
|
|
/**
|
|
* Replace the cache with a fresh schedule from backend.
|
|
*/
|
|
sync(date: string, slots: CalendarSlotResponse[]): void {
|
|
// If date changed, clear old data
|
|
if (this.cachedDate !== date) {
|
|
this.slots.clear();
|
|
}
|
|
this.cachedDate = date;
|
|
|
|
// Merge: update existing slots, add new ones
|
|
const incomingIds = new Set<string>();
|
|
for (const slot of slots) {
|
|
const id = this.getSlotId(slot);
|
|
incomingIds.add(id);
|
|
this.slots.set(id, slot);
|
|
}
|
|
|
|
// Remove slots that no longer exist on backend (cancelled etc.)
|
|
for (const id of this.slots.keys()) {
|
|
if (!incomingIds.has(id)) {
|
|
this.slots.delete(id);
|
|
}
|
|
}
|
|
|
|
this.lastSyncAt = new Date();
|
|
}
|
|
|
|
/**
|
|
* Get slots that are due (scheduled_at <= now) and still pending.
|
|
*/
|
|
getDueSlots(now: Date): CalendarSlotResponse[] {
|
|
const results: CalendarSlotResponse[] = [];
|
|
for (const slot of this.slots.values()) {
|
|
if (slot.status !== "not_started" && slot.status !== "deferred") continue;
|
|
if (!slot.scheduled_at) continue;
|
|
|
|
const scheduledAt = this.parseScheduledTime(slot.scheduled_at);
|
|
if (scheduledAt && scheduledAt <= now) {
|
|
results.push(slot);
|
|
}
|
|
}
|
|
// Sort by priority descending
|
|
results.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Update a slot locally (e.g., after status change).
|
|
*/
|
|
updateSlot(slotId: string, update: Partial<CalendarSlotResponse>): void {
|
|
const existing = this.slots.get(slotId);
|
|
if (existing) {
|
|
this.slots.set(slotId, { ...existing, ...update });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a slot from cache.
|
|
*/
|
|
removeSlot(slotId: string): void {
|
|
this.slots.delete(slotId);
|
|
}
|
|
|
|
/**
|
|
* Get all cached slots.
|
|
*/
|
|
getAll(): CalendarSlotResponse[] {
|
|
return Array.from(this.slots.values());
|
|
}
|
|
|
|
/**
|
|
* Get cache metadata.
|
|
*/
|
|
getStatus(): { slotCount: number; lastSyncAt: string | null; cachedDate: string | null } {
|
|
return {
|
|
slotCount: this.slots.size,
|
|
lastSyncAt: this.lastSyncAt?.toISOString() ?? null,
|
|
cachedDate: this.cachedDate,
|
|
};
|
|
}
|
|
|
|
private getSlotId(slot: CalendarSlotResponse): string {
|
|
return slot.virtual_id ?? String(slot.id);
|
|
}
|
|
|
|
private parseScheduledTime(scheduledAt: string): Date | null {
|
|
// scheduled_at can be "HH:MM:SS" (time only) or full ISO
|
|
if (/^\d{2}:\d{2}(:\d{2})?$/.test(scheduledAt)) {
|
|
// Time-only: combine with cached date
|
|
if (!this.cachedDate) return null;
|
|
return new Date(`${this.cachedDate}T${scheduledAt}Z`);
|
|
}
|
|
const d = new Date(scheduledAt);
|
|
return isNaN(d.getTime()) ? null : d;
|
|
}
|
|
}
|