Diagnostics Catalog
The runtime emits a diagnostic for every interesting thing it does. Subscribe with runtime.onDiagnostic(handler). The shape is always:
type KitDiagnostic = { type: string timestamp: number detail?: Record<string, unknown>}Catalog
Section titled “Catalog”Event lifecycle
Section titled “Event lifecycle”| Type | When | detail |
|---|---|---|
event.emitted | After runtime.emit() builds the event but before handlers run | { eventType, event } |
Command lifecycle
Section titled “Command lifecycle”| Type | When | detail |
|---|---|---|
command.handler_registered | After runtime.handleCommand() adds a handler | { commandType } |
command.dispatched | When runtime.command() is called | { commandType, command } |
command.handled | After a handler returns successfully | { commandType } |
command.failed | When a handler throws or returns rejected | { commandType, error } |
command.unhandled | When no handler is registered for the command | { commandType } |
Module lifecycle
Section titled “Module lifecycle”| Type | When | detail |
|---|---|---|
module.installed | After runtime.install(module) finishes setup and registration | { moduleName } |
runtime.started | After all module.start hooks complete | {} |
runtime.stopped | After all module.stop hooks complete (in reverse order) | {} |
Provider lifecycle
Section titled “Provider lifecycle”| Type | When | detail |
|---|---|---|
provider.overwritten | When a provider token is assigned twice | { provider } (description) |
Boundary lifecycle
Section titled “Boundary lifecycle”| Type | When | detail |
|---|---|---|
boundary.created | When runtime.createBoundary() is called | { context } |
boundary.destroyed | When a boundary handle’s destroy() runs | { context } |
Subscribing
Section titled “Subscribing”const unsubscribe = runtime.onDiagnostic((d) => { console.debug(`[${d.type}]`, d.detail)})
// laterunsubscribe()Subscribers are independent. Throwing inside one does not prevent others from running.
The debug module
Section titled “The debug module”The simplest way to surface diagnostics during development is the debug module:
import { debugModule } from '@atheory-ai/kitsune-dev'shell.modules = [...modules, debugModule()]It logs every event (via events: { '*': ... }) and every diagnostic (via runtime.onDiagnostic).
Building a custom diagnostic stream
Section titled “Building a custom diagnostic stream”For richer dev tooling — a timeline UI, a state replay tool, a diff between expected and actual — use the diagnostic stream as input:
const buffer: KitDiagnostic[] = []
runtime.onDiagnostic((d) => { buffer.push(d) if (buffer.length > 1000) buffer.shift()})
// Expose to a future devtools panel;(window as never).__kitsune_diagnostics = bufferFuture devtools integrations will read from a similar buffer.
Shipping diagnostics to production
Section titled “Shipping diagnostics to production”Diagnostics are designed for development. In production:
- Don’t install the debug module. It logs to the console.
- Don’t subscribe at all unless you have a use case (e.g., capturing failed commands for an error reporting service).
- If you do subscribe, sample. A high-traffic app emits hundreds of diagnostics per minute.
The runtime always emits diagnostics; the cost is one Map lookup per emit. With no subscribers, nothing is logged.
A diagnostic-driven test pattern
Section titled “A diagnostic-driven test pattern”Diagnostics are useful in tests for asserting that something happened the way you expected:
const seen: string[] = []runtime.onDiagnostic((d) => seen.push(d.type))
await runtime.command({ type: 'note.create', payload: { title: 'Hello' } })
expect(seen).toContain('command.dispatched')expect(seen).toContain('command.handled')This is a low-coupling alternative to mocking — you assert the runtime’s behavior, not the implementation’s call shape.