summaryrefslogtreecommitdiffhomepage
path: root/packages/cache-warming/src/extension.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 12:23:06 +0900
committerAdam Malczewski <[email protected]>2026-06-11 12:23:06 +0900
commitc2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (patch)
tree3f7c2feddbe697a79abd952bb80ed0e01dac0a7a /packages/cache-warming/src/extension.ts
parentf6b45507210e04e9884256b0132900640de4334b (diff)
downloaddispatch-c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3.tar.gz
dispatch-c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3.zip
feat(cache-warming): per-conversation prompt-cache warming + warm() service
Backend-driven warming targeting whatever provider a conversation uses (incl. the external Claude provider-anthropic). Core engine + on/off + last-cache-% done; interval-as-view-control pending a ui-contract NumberField (surface-system gap). Mechanism: - kernel: expose HostAPI.emit (typed bus event emit; counterpart of on) - session-orchestrator: turnStarted/turnSettled event hooks (conversationId/cwd/model); warm() service (cacheWarmHandle) reusing the real-turn assembly (byte-identical prefix, provider-agnostic), refuses mid-turn, never persists/emits, returns Usage - cache-warming (new ext): per-conversation timers (arm on settle, cancel on start, in-flight invalidation), calls warm(), pct=round(clamp(cacheRead/input,0,1)*100), persists {enabled,intervalMs} (default on/240s), registers a controls surface - host-bin: register cache-warming; transport-http: HostAPI stub +emit (fan-out) Honors old-code invariants. 760 vitest + 109 bun = 869 tests; tsc -b EXIT 0; biome clean.
Diffstat (limited to 'packages/cache-warming/src/extension.ts')
-rw-r--r--packages/cache-warming/src/extension.ts130
1 files changed, 130 insertions, 0 deletions
diff --git a/packages/cache-warming/src/extension.ts b/packages/cache-warming/src/extension.ts
new file mode 100644
index 0000000..16515a8
--- /dev/null
+++ b/packages/cache-warming/src/extension.ts
@@ -0,0 +1,130 @@
+import type { Extension, HostAPI, Manifest } from "@dispatch/kernel";
+import { cacheWarmHandle, turnSettled, turnStarted } from "@dispatch/session-orchestrator";
+import type { SurfaceProvider } from "@dispatch/surface-registry";
+import { surfaceRegistryHandle } from "@dispatch/surface-registry";
+import type { SurfaceSpec } from "@dispatch/ui-contract";
+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"],
+ },
+};
+
+function buildSurfaceSpec(
+ _conversationId: string | undefined,
+ enabled: boolean,
+ lastPct: number | null,
+): SurfaceSpec {
+ const pctDisplay = lastPct === null ? "—" : `${lastPct}%`;
+ return {
+ id: "cache-warming",
+ region: "side",
+ title: "Cache Warming",
+ fields: [
+ {
+ kind: "toggle",
+ label: "Enabled",
+ value: enabled,
+ action: { actionId: "cache-warming/toggle" },
+ },
+ {
+ kind: "stat",
+ label: "Last Cache %",
+ value: pctDisplay,
+ },
+ ],
+ };
+}
+
+export function activate(host: HostAPI): void {
+ const warmService = host.getService(cacheWarmHandle);
+ const registry = host.getService(surfaceRegistryHandle);
+ const storage = host.storage("cache-warming");
+
+ let currentConversationId: string | undefined;
+
+ // Timer wrapper: setTimeout/clearTimeout return Timeout in Node types,
+ // but our TimerDeps uses number ids. Map between them.
+ const timeoutMap = new Map<number, ReturnType<typeof setTimeout>>();
+ 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);
+ }
+ },
+ },
+ onSurfaceChange: () => {
+ // Surface subscribers will re-fetch on next getSpec()
+ },
+ });
+
+ host.on(turnStarted, (payload) => {
+ currentConversationId = payload.conversationId;
+ warmer.onTurnStarted(payload.conversationId);
+ });
+
+ host.on(turnSettled, (payload) => {
+ currentConversationId = payload.conversationId;
+ warmer.onTurnSettled(payload.conversationId, {
+ ...(payload.cwd !== undefined ? { cwd: payload.cwd } : {}),
+ ...(payload.modelName !== undefined ? { modelName: payload.modelName } : {}),
+ });
+ });
+
+ const provider: SurfaceProvider = {
+ catalogEntry: {
+ id: "cache-warming",
+ region: "side",
+ title: "Cache Warming",
+ },
+ getSpec() {
+ const convId = currentConversationId;
+ const state =
+ convId !== undefined ? warmer.getState(convId) : { enabled: true, lastPct: null };
+ return buildSurfaceSpec(convId, state.enabled, state.lastPct);
+ },
+ async invoke(actionId, payload) {
+ const pl = payload as Record<string, unknown> | undefined;
+ const convId =
+ (typeof pl?.conversationId === "string" ? pl.conversationId : undefined) ??
+ currentConversationId;
+ if (convId === undefined) return;
+
+ if (actionId === "cache-warming/toggle") {
+ const current = warmer.getState(convId);
+ await warmer.setEnabled(convId, !current.enabled);
+ }
+ },
+ };
+
+ registry.register(provider);
+
+ host.logger.info("cache-warming: registered");
+}
+
+export const extension: Extension = {
+ manifest,
+ activate,
+};