From c2d00c18a788d4651e41e73777eec2494a943816 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 23 May 2026 14:58:37 +0100 Subject: [PATCH] feat(hf-plugin): __hfAgentStatus.hasOnCallCovering(agentId, from, to) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-plugin accessor for "does agent have on_call slot covering this window?" — first consumer is Dialectic.OpenclawPlugin signup pre-check (its hf-precheck.ts has been degrading to "skipped" since Phase 3 ship pending this). v1 honest scope: same-day windows only (scheduleCache is today-only from /calendar/sync). Cross-day or empty-cache windows return undefined which the caller treats as "skipped" (Dialectic backend stores pre_validated:false as audit signal — same as before, just now we actually validate when we can). Logic: for each cached slot where slot_type=on_call AND status not in {aborted,cancelled}, parse scheduled_at (HH:MM:SS or full ISO) and estimated_duration to compute end; return true iff start<=from AND end>=to. Returns false (not undefined) when cache has slots for the agent on this date but none covers — that means "actually no coverage" vs "I dont know". Pairs with Dialectic.OpenclawPlugin/src/hf-precheck.ts which already calls hf.hasOnCallCovering and handles all 3 return shapes. No backend change required. --- plugin/index.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) 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