import type { Extension, HostAPI, Manifest } from "@dispatch/kernel"; import { cacheWarmHandle, conversationClosed, turnSettled, turnStarted, warmCompleted, } from "@dispatch/session-orchestrator"; import type { SurfaceContext, SurfaceProvider } from "@dispatch/surface-registry"; import { surfaceRegistryHandle } from "@dispatch/surface-registry"; import type { SurfaceSpec } from "@dispatch/ui-contract"; import { buildConversationSpec, buildDefaultSpec, parseIntervalPayload, secondsToMs, } from "./pure.js"; import { createCacheWarmer } from "./warmer.js"; export const manifest: Manifest = { id: "cache-warming", name: "Cache Warming", version: "0.0.0", apiVersion: "^0.1.0", trust: "bundled", activation: "eager", dependsOn: ["session-orchestrator", "surface-registry"], capabilities: {}, contributes: { services: ["cache-warming/surface"], }, }; export function activate(host: HostAPI): void { const warmService = host.getService(cacheWarmHandle); const registry = host.getService(surfaceRegistryHandle); const storage = host.storage("cache-warming"); const subscribers = new Set<() => void>(); const timeoutMap = new Map>(); let nextTimerId = 1; const warmer = createCacheWarmer({ warm: warmService.warm, storage, logger: host.logger, timers: { setTimer(fn, ms) { const id = nextTimerId++; timeoutMap.set(id, setTimeout(fn, ms)); return id; }, clearTimer(id) { const timeout = timeoutMap.get(id); if (timeout !== undefined) { clearTimeout(timeout); timeoutMap.delete(id); } }, }, now: () => Date.now(), onSurfaceChange: () => { for (const notify of subscribers) { notify(); } }, }); host.on(turnStarted, (payload) => { warmer.onTurnStarted(payload.conversationId); }); host.on(turnSettled, (payload) => { warmer.onTurnSettled(payload.conversationId, { ...(payload.cwd !== undefined ? { cwd: payload.cwd } : {}), ...(payload.modelName !== undefined ? { modelName: payload.modelName } : {}), }); }); host.on(warmCompleted, (payload) => { warmer.onWarmCompleted(payload); }); host.on(conversationClosed, (payload) => { // Sync part (cancel + disable) runs before the first await inside; // only the settings persist is deferred. void warmer.onConversationClosed(payload.conversationId); }); function getSpec(context?: SurfaceContext): SurfaceSpec { const convId = context?.conversationId; if (convId === undefined) { return buildDefaultSpec(); } const state = warmer.getState(convId); return buildConversationSpec( state.enabled, state.intervalMs, state.lastPct, state.lastExpectedPct, state.nextWarmAt, state.lastWarmAt, ); } async function invoke( actionId: string, payload?: unknown, context?: SurfaceContext, ): Promise { const convId = context?.conversationId; if (convId === undefined) return; if (actionId === "cache-warming/toggle") { const current = warmer.getState(convId); await warmer.setEnabled(convId, !current.enabled); } if (actionId === "cache-warming/set-interval") { const seconds = parseIntervalPayload(payload); if (seconds === null) return; const ms = secondsToMs(seconds); if (ms === null) return; await warmer.setIntervalMs(convId, ms); } } const provider: SurfaceProvider = { catalogEntry: { id: "cache-warming", region: "side", title: "Cache Warming", scope: "conversation", }, getSpec, invoke, subscribe(onChange) { subscribers.add(onChange); return () => { subscribers.delete(onChange); }; }, }; registry.register(provider); host.logger.info("cache-warming: registered"); } export const extension: Extension = { manifest, activate, };