summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src
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/kernel/src
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/kernel/src')
-rw-r--r--packages/kernel/src/contracts/extension.ts12
-rw-r--r--packages/kernel/src/host/host.test.ts43
-rw-r--r--packages/kernel/src/host/host.ts3
3 files changed, 58 insertions, 0 deletions
diff --git a/packages/kernel/src/contracts/extension.ts b/packages/kernel/src/contracts/extension.ts
index 5a7821b..4d6cf07 100644
--- a/packages/kernel/src/contracts/extension.ts
+++ b/packages/kernel/src/contracts/extension.ts
@@ -190,6 +190,18 @@ export interface HostAPI {
handler: EventHandler<TPayload>,
) => () => void;
+ /**
+ * Emit an event hook: fire-and-forget dispatch to every `on` subscriber,
+ * error-isolated per handler (a thrown handler is caught + logged, never
+ * breaks the caller). The counterpart of `on`.
+ *
+ * This lets a core extension that OWNS a lifecycle publish typed events that
+ * standard extensions react to — e.g. the session-orchestrator emitting
+ * per-turn start/settle events a cache-warming extension subscribes to. The
+ * kernel owns the mechanism; the owner declares the typed `EventHookDescriptor`.
+ */
+ readonly emit: <TPayload>(hook: EventHookDescriptor<TPayload>, payload: TPayload) => void;
+
/** Add a filter to a filter hook chain. Filters are awaited in-band. */
readonly addFilter: <TValue>(
hook: FilterDescriptor<TValue>,
diff --git a/packages/kernel/src/host/host.test.ts b/packages/kernel/src/host/host.test.ts
index 669d093..0067091 100644
--- a/packages/kernel/src/host/host.test.ts
+++ b/packages/kernel/src/host/host.test.ts
@@ -617,6 +617,49 @@ describe("createHost", () => {
expect(received).toEqual(["hello"]);
});
+ it("emit dispatches to handlers registered via on", async () => {
+ const hook = defineEventHook<string>("test/emit-dispatch");
+ const received: string[] = [];
+
+ const ext = createExtension("emit-ext", {
+ activate: (host) => {
+ host.on(hook, (payload) => {
+ received.push(payload);
+ });
+ },
+ });
+
+ const host = createHost([ext], deps);
+ await host.activate();
+
+ const api = host.getHostAPI();
+ api.emit(hook, "world");
+ expect(received).toEqual(["world"]);
+ });
+
+ it("emit isolates a throwing handler (does not propagate)", async () => {
+ const hook = defineEventHook<string>("test/emit-isolation");
+ const received: string[] = [];
+
+ const ext = createExtension("emit-isolation-ext", {
+ activate: (host) => {
+ host.on(hook, () => {
+ throw new Error("handler boom");
+ });
+ host.on(hook, (payload) => {
+ received.push(payload);
+ });
+ },
+ });
+
+ const host = createHost([ext], deps);
+ await host.activate();
+
+ const api = host.getHostAPI();
+ expect(() => api.emit(hook, "safe")).not.toThrow();
+ expect(received).toEqual(["safe"]);
+ });
+
it("applyFilters threads a value through registered filters in order", async () => {
const hook = defineFilter<string>("test/text-transform");
diff --git a/packages/kernel/src/host/host.ts b/packages/kernel/src/host/host.ts
index a6396a9..2a262be 100644
--- a/packages/kernel/src/host/host.ts
+++ b/packages/kernel/src/host/host.ts
@@ -122,6 +122,9 @@ export function createHost(extensions: readonly Extension[], deps: HostDeps): Ho
on<TPayload>(hook: EventHookDescriptor<TPayload>, handler: EventHandler<TPayload>) {
return deps.bus.on(hook, handler);
},
+ emit<TPayload>(hook: EventHookDescriptor<TPayload>, payload: TPayload) {
+ deps.bus.emit(hook, payload);
+ },
addFilter<TValue>(hook: FilterDescriptor<TValue>, fn: FilterHandler<TValue>) {
return deps.bus.addFilter(hook, fn);
},