Custom Handlers
This guide explains how to create custom event handlers for the Crucible event system. For the simpler hook-based approach, see Event Hooks.
Handler Types
Section titled “Handler Types”Crucible supports two types of handlers:
- Rust Handlers: Compiled handlers with full access to the Rust ecosystem
- Lua Handlers: Scripted handlers for user customization without recompilation
Rust Handlers
Section titled “Rust Handlers”Basic Structure
Section titled “Basic Structure”use crucible_core::events::{SessionEvent, SharedEventBus};use std::sync::Arc;
pub struct MyHandler { // Handler state (e.g., database connection, service reference) service: Arc<MyService>, emitter: SharedEventBus<SessionEvent>,}
impl MyHandler { /// Handler priority (lower runs first) pub const PRIORITY: u32 = 150;
pub fn new(service: Arc<MyService>, emitter: SharedEventBus<SessionEvent>) -> Self { Self { service, emitter } }
/// Handle a NoteParsed event async fn handle_note_parsed(&self, path: &str, block_count: usize) -> Result<()> { // Your processing logic here self.service.process(path).await?;
// Optionally emit downstream events self.emitter.emit(SessionEvent::Custom { name: "my_handler_complete".to_string(), payload: serde_json::json!({ "path": path }), }).await?;
Ok(()) }}Built-in Handler Examples
Section titled “Built-in Handler Examples”StorageHandler
Section titled “StorageHandler”Handles database persistence:
// From crucible-sqlite/src/event_handlers/storage_handler.rs
pub struct StorageHandler { store: Arc<EAVGraphStore>, emitter: SharedEventBus<SessionEvent>,}
impl StorageHandler { pub const PRIORITY: u32 = 100;
async fn handle_note_parsed(&self, event: &SessionEvent) -> Result<()> { if let SessionEvent::NoteParsed { path, payload, .. } = event { let entity_id = self.store.upsert_note(path, payload).await?;
self.emitter.emit(SessionEvent::EntityStored { entity_id: entity_id.clone(), entity_type: EventEntityType::Note, }).await?; } Ok(()) }}Lua Handlers
Section titled “Lua Handlers”Lua handlers are scripts that process events without requiring Rust compilation.
Location
Section titled “Location”Place Lua handler files in:
{kiln}/.crucible/handlers/*.luaBasic Structure
Section titled “Basic Structure”-- my_handler.lua
--- Handle events-- @handler event="note:parsed" pattern="*" priority=100function handle_note_parsed(ctx, event) cru.log("info", "Note parsed: " .. event.identifier) return eventend
--- Handle file changes-- @handler event="file:changed" pattern="*" priority=100function handle_file_changed(ctx, event) cru.log("info", "File changed: " .. event.identifier) return eventendEvent API in Lua
Section titled “Event API in Lua”Events expose fields for common operations:
-- @handler event="note:parsed" pattern="*" priority=100function handle(ctx, event) -- Get event metadata local event_type = event.event_type -- "note:parsed", "file:changed", etc. local identifier = event.identifier -- Path or entity ID
-- Access payload local tags = event.payload.tags local content = event.payload.content
return eventendCancelling Events
Section titled “Cancelling Events”Handlers can cancel preventable events:
-- @handler event="tool:before" pattern="*" priority=5function block_secrets(ctx, event) if string.find(event.identifier, ".secret") then cru.log("warn", "Blocked access to secret file") event.cancelled = true end return eventendEmitting Custom Events
Section titled “Emitting Custom Events”-- @handler event="note:parsed" pattern="*" priority=100function handle(ctx, event) -- Process event process_note(event)
-- Emit custom event ctx:emit("my_handler_done", { source = event.identifier, timestamp = os.time() })
return eventendTesting Handlers
Section titled “Testing Handlers”Unit Tests
Section titled “Unit Tests”#[tokio::test]async fn test_my_handler() { use crucible_core::events::NoOpEmitter;
let emitter = Arc::new(NoOpEmitter::new()); let handler = MyHandler::new(service, emitter);
let result = handler.handle_note_parsed("test.md", 5).await; assert!(result.is_ok());}Integration Tests
Section titled “Integration Tests”#[tokio::test]async fn test_handler_in_event_system() { use crucible_cli::event_system::initialize_event_system;
let temp_dir = TempDir::new()?; let config = create_test_config(temp_dir.path().to_path_buf());
let handle = initialize_event_system(&config).await?;
std::fs::write(temp_dir.path().join("test.md"), "# Test\n\nContent")?;
tokio::time::sleep(Duration::from_millis(500)).await;
// Verify expected outcomes handle.shutdown().await?;}Best Practices
Section titled “Best Practices”1. Use Appropriate Priority
Section titled “1. Use Appropriate Priority”| Range | Use |
|---|---|
| 50-99 | Pre-processing hooks |
| 100-199 | Core data handlers (storage, tags) |
| 200-299 | Enrichment handlers (embeddings) |
| 300-499 | Analytics/reporting |
| 500+ | Custom user handlers |
2. Fail Gracefully
Section titled “2. Fail Gracefully”async fn handle_event(&self, event: &SessionEvent) -> Result<()> { match self.process(event).await { Ok(_) => Ok(()), Err(e) => { // Log but don't fail the cascade warn!("Handler error (non-fatal): {}", e); Ok(()) } }}3. Emit Downstream Events
Section titled “3. Emit Downstream Events”Keep the cascade flowing by emitting appropriate events:
// After storing entityself.emitter.emit(SessionEvent::EntityStored { ... }).await?;
// After updating blocksself.emitter.emit(SessionEvent::BlocksUpdated { ... }).await?;4. Avoid Blocking Operations
Section titled “4. Avoid Blocking Operations”Use async/await for I/O operations:
// Good: Async I/Olet result = self.database.query(sql).await?;
// Bad: Blocking I/Olet result = std::fs::read_to_string(path)?; // Blocks the async runtime5. Handle Event Types Explicitly
Section titled “5. Handle Event Types Explicitly”async fn handle(&self, event: &SessionEvent) -> Result<()> { match event { SessionEvent::NoteParsed { path, .. } => { self.handle_note_parsed(path).await } SessionEvent::FileDeleted { path } => { self.handle_file_deleted(path).await } _ => Ok(()), // Ignore other event types }}Handler Lifecycle
Section titled “Handler Lifecycle”- Registration: Handlers are registered during
initialize_event_system() - Execution: Handlers execute in priority order when events are emitted
- Cascade: Handlers can emit new events, triggering further handlers
- Shutdown: Handlers are dropped when the EventBus is dropped
Troubleshooting
Section titled “Troubleshooting”Handler Not Executing
Section titled “Handler Not Executing”- Check event type matches handler subscription
- Verify priority allows handler to run
- Check pattern matching (glob syntax)
- Enable debug logging:
RUST_LOG=crucible_cli=debug
Events Not Propagating
Section titled “Events Not Propagating”- Ensure handlers return the event (not cancel it)
- Check for fatal errors in handler chain
- Verify emitter is properly configured
Lua Handler Errors
Section titled “Lua Handler Errors”- Check syntax with
lua -p handlers/*.lua - Verify handler function signature
- Check for runtime errors in logs
See Also
Section titled “See Also”- Event Hooks - Simpler hook-based approach
- Event Architecture - Internal event system design
- Language Basics - Lua syntax