diff --git a/plugin/index.ts b/plugin/index.ts index c1cc19a..9e89996 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -424,6 +424,64 @@ function register(api: PluginAPI): void { } return cached?.status; }, + + /** + * Approximate "does agent have an on_call slot covering [from, to]?" + * for cross-plugin pre-check use (currently: + * Dialectic.OpenclawPlugin's signup HF coverage). + * + * v1 honest scope: we only have today's slots in scheduleCache + * (synced from /calendar/sync which is today-only). Returns: + * - true iff window is same-day AND some cached on_call slot + * starts <= from AND ends >= to + * - false iff window is same-day AND no such slot + * - undefined for cross-day windows OR cache empty for this + * agent (caller treats undefined as "I don't know" — see + * Dialectic plugin's hf-precheck.ts which degrades to + * "skipped" gracefully) + * + * Phase TBD: when HF backend ships a `/calendar/slots?agent&from&to` + * endpoint, swap this to call it for arbitrary windows. Until then, + * same-day-only coverage gates ~all debates created by analyze-intel + * (which schedules <2h windows) without needing a backend change. + */ + async hasOnCallCovering( + agentId: string, + fromIso: string, + toIso: string, + ): Promise { + if (!agentId || !fromIso || !toIso) return undefined; + const from = new Date(fromIso); + const to = new Date(toIso); + if (isNaN(from.getTime()) || isNaN(to.getTime())) return undefined; + if (!(from < to)) return undefined; + // Cross-day → cache only has today; can't decide. + const fromDate = from.toISOString().slice(0, 10); + const toDate = to.toISOString().slice(0, 10); + if (fromDate !== toDate) return undefined; + // Cache's cachedDate must match our window's date. + const cacheStatus = scheduleCache.getStatus(); + if (cacheStatus.cachedDate !== fromDate) return undefined; + const slots = scheduleCache.getAgentSlots(agentId); + if (slots.length === 0) return undefined; // cache empty for this agent — can't decide + for (const s of slots) { + if (s.slot_type !== 'on_call') continue; + // status: ignore aborted/cancelled, accept not_started / ongoing / finished + if (s.status === 'aborted' || s.status === 'cancelled') continue; + const startStr = s.scheduled_at; + if (typeof startStr !== 'string') continue; + // scheduled_at can be HH:MM:SS (cache-relative date) or full ISO + const start = + /^\d{2}:\d{2}(:\d{2})?$/.test(startStr) + ? new Date(`${fromDate}T${startStr}Z`) + : new Date(startStr); + if (isNaN(start.getTime())) continue; + const dur = typeof s.estimated_duration === 'number' ? s.estimated_duration : 0; + const end = new Date(start.getTime() + dur * 60_000); + if (start <= from && end >= to) return true; + } + return false; + }, }; // Track wakes already dispatched for a slot in the current sync