Skip to content

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>
}
TypeWhendetail
event.emittedAfter runtime.emit() builds the event but before handlers run{ eventType, event }
TypeWhendetail
command.handler_registeredAfter runtime.handleCommand() adds a handler{ commandType }
command.dispatchedWhen runtime.command() is called{ commandType, command }
command.handledAfter a handler returns successfully{ commandType }
command.failedWhen a handler throws or returns rejected{ commandType, error }
command.unhandledWhen no handler is registered for the command{ commandType }
TypeWhendetail
module.installedAfter runtime.install(module) finishes setup and registration{ moduleName }
runtime.startedAfter all module.start hooks complete{}
runtime.stoppedAfter all module.stop hooks complete (in reverse order){}
TypeWhendetail
provider.overwrittenWhen a provider token is assigned twice{ provider } (description)
TypeWhendetail
boundary.createdWhen runtime.createBoundary() is called{ context }
boundary.destroyedWhen a boundary handle’s destroy() runs{ context }
const unsubscribe = runtime.onDiagnostic((d) => {
console.debug(`[${d.type}]`, d.detail)
})
// later
unsubscribe()

Subscribers are independent. Throwing inside one does not prevent others from running.

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).

debugModule reference →

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 = buffer

Future devtools integrations will read from a similar buffer.

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.

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.