diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 12:23:06 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 12:23:06 +0900 |
| commit | c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (patch) | |
| tree | 3f7c2feddbe697a79abd952bb80ed0e01dac0a7a /packages/cache-warming/src/pure.ts | |
| parent | f6b45507210e04e9884256b0132900640de4334b (diff) | |
| download | dispatch-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/pure.ts')
| -rw-r--r-- | packages/cache-warming/src/pure.ts | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/packages/cache-warming/src/pure.ts b/packages/cache-warming/src/pure.ts new file mode 100644 index 0000000..2b00dab --- /dev/null +++ b/packages/cache-warming/src/pure.ts @@ -0,0 +1,95 @@ +/** + * Pure core for cache-warming — zero I/O, zero ambient state. + * Every function is input → output; testable without mocks. + */ + +// --- Types --- + +/** Persisted per-conversation settings (storage-facing). */ +export interface ConversationSettings { + readonly enabled: boolean; + readonly intervalMs: number; +} + +/** Full per-conversation runtime state (in-memory, not persisted). */ +export interface ConversationState extends ConversationSettings { + readonly active: boolean; + readonly lastPct: number | null; + readonly token: number; +} + +/** Context stored per-conversation from the latest lifecycle event. */ +export interface ConversationContext { + readonly cwd?: string; + readonly modelName?: string; +} + +export const DEFAULT_INTERVAL_MS = 240_000; +export const MIN_INTERVAL_MS = 1000; + +// --- Pure functions --- + +/** + * Compute cache-hit percentage from token counts. + * Returns an integer in [0, 100]. inputTokens ≤ 0 → 0. + */ +export function computeCachePct(inputTokens: number, cacheReadTokens: number): number { + if (inputTokens <= 0) return 0; + const ratio = cacheReadTokens / inputTokens; + const clamped = Math.max(0, Math.min(1, ratio)); + return Math.round(clamped * 100); +} + +/** + * Decide whether a conversation should be warmed right now. + * Requires: enabled, idle (not active), and the token is current (not superseded). + */ +export function shouldWarm(state: ConversationState, currentToken: number): boolean { + return state.enabled && !state.active && state.token === currentToken; +} + +/** + * Check whether a token is still current (not superseded by a newer cancel/fire). + */ +export function isTokenCurrent(current: number, expected: number): boolean { + return current === expected; +} + +const SETTINGS_KEY = "settings"; + +/** + * Parse settings from a raw storage string. + * Returns defaults if null or malformed. + */ +export function parseSettings(raw: string | null): ConversationSettings { + if (raw === null) return { enabled: true, intervalMs: DEFAULT_INTERVAL_MS }; + try { + const parsed: unknown = JSON.parse(raw); + if (typeof parsed !== "object" || parsed === null) { + return { enabled: true, intervalMs: DEFAULT_INTERVAL_MS }; + } + const obj = parsed as Record<string, unknown>; + const enabled = typeof obj.enabled === "boolean" ? obj.enabled : true; + const rawInterval = obj.intervalMs; + let intervalMs = DEFAULT_INTERVAL_MS; + if (typeof rawInterval === "number" && Number.isFinite(rawInterval)) { + intervalMs = + rawInterval <= 0 ? MIN_INTERVAL_MS : Math.max(MIN_INTERVAL_MS, Math.round(rawInterval)); + } + return { enabled, intervalMs }; + } catch { + return { enabled: true, intervalMs: DEFAULT_INTERVAL_MS }; + } +} + +/** + * Serialize settings for storage. + */ +export function serializeSettings(settings: ConversationSettings): string { + return JSON.stringify(settings); +} + +/** The storage key for a conversation's settings. */ +export function settingsKey(conversationId: string): string { + return `${SETTINGS_KEY}:${conversationId}`; +} |
