Daemon Integration & Cross-Process Events
Version: 2.3.0+ Status: Available - Production Ready
Overview
NCP 2.3.0 integrates with the photon daemon for:
- Cross-process event routing - Photons in different processes coordinate
- Event distribution - Events flow through DaemonBroker (Unix socket)
- Graceful fallback - Local-only mode when daemon not running
- Zero setup overhead - Works automatically, no configuration needed
Architecture
Without Daemon (Local-Only)
NCP Process
├── Photon A → emits event
├── Photon B → subscribes
└── Event routed locally (NoOpBroker)Limitation: Events only work within single process
With Daemon (Cross-Process)
Photon Daemon
│
├── Unix Socket
│ │
│ ├── NCP Process 1
│ │ ├── Photon A → emits event
│ │ └── Photon C (no subscription)
│ │
│ └── NCP Process 2
│ ├── Photon B → subscribes
│ └── Receives event from Photon ABenefit: Events flow across processes transparently
Using Cross-Process Events
Publishing Events
Any Photon can emit events:
export default class DataProcessor extends Photon {
async run(params) {
const data = await fetchData();
// Emit event (goes to daemon if running, local if not)
await this.emit({
type: 'data_ready',
data: {
count: data.length,
timestamp: new Date().toISOString()
}
});
return { processed: true };
}
}Subscribing to Events
Declare subscriptions with @notify-on annotation:
/**
* Photon that listens for data_ready events
* @notify-on data_ready,file_updated
*/
export default class DataListener extends Photon {
async onNotification(eventType, payload) {
if (eventType === 'data_ready') {
console.log(`Data ready: ${payload.count} items`);
// React to event
}
}
}How It Works
- Photon declares subscription via
@notify-ontag - NCP loads Photon and registers subscription
- Another Photon emits event via
this.emit() - NCP dispatches locally to subscribed Photons
- NCP publishes to daemon for cross-process delivery
- Daemon routes to other processes via DaemonBroker
- Remote Photons receive via
onNotification()
Setting Up Photon Daemon
Option 1: Separate Daemon Process
# Terminal 1: Start daemon
photon daemon
# Terminal 2: NCP uses it automatically
ncp find "search query"Option 2: Beam UI (includes daemon)
# Starts photon daemon internally
photon beamVerification
Check if daemon is running:
# If daemon running: events will be cross-process
# If daemon not running: events stay local (NoOpBroker fallback)
# No configuration needed - works automatically!Event Publishing Examples
Example 1: Data Processing Pipeline
Processor Photon - Fetches data:
export default class DataFetcher extends Photon {
async run(params) {
const data = await fetchFromAPI();
// Emit event
await this.emit({
type: 'data_fetched',
data: { recordCount: data.length }
});
return { fetched: true };
}
}Listener Photon - Reacts to data:
/**
* @notify-on data_fetched
*/
export default class DataImporter extends Photon {
async onNotification(eventType, payload) {
if (eventType === 'data_fetched') {
const { recordCount } = payload;
console.log(`Importing ${recordCount} records...`);
// Import data
await this.importRecords(recordCount);
}
}
}Workflow:
- Run
data-fetcherPhoton → emitsdata_fetched - Daemon routes event to subscribers
data-importerreceives event → imports data- Both Photons coordinate without direct coupling
Example 2: Metrics Collection
Stats Photon - Publishes metrics:
export default class MetricsCollector extends Photon {
async run(params) {
const metrics = {
cpuUsage: process.cpuUsage(),
memoryUsage: process.memoryUsage(),
timestamp: new Date()
};
await this.emit({
type: 'metrics_collected',
data: metrics
});
return metrics;
}
}Monitor Photon - Tracks metrics:
/**
* @notify-on metrics_collected
*/
export default class MetricsMonitor extends Photon {
private state = { maxMemory: 0, alerts: [] };
getState() { return this.state; }
setState(newState) { this.state = newState; }
async onNotification(eventType, payload) {
if (eventType === 'metrics_collected') {
const { memoryUsage } = payload.data;
// Track memory
if (memoryUsage.heapUsed > this.state.maxMemory) {
this.state.maxMemory = memoryUsage.heapUsed;
this.state.alerts.push({
type: 'high_memory',
value: memoryUsage.heapUsed,
timestamp: new Date()
});
}
}
}
}File Watching Improvements
PhotonWatcher Replacement
NCP 2.3.0 replaces chokidar with PhotonWatcher from photon-core:
Benefits:
- ✅ Symlink resolution (macOS compatibility)
- ✅ Debouncing built-in (prevents reload storms)
- ✅ Editor temp file filtering (ignores
.swp,.bak, etc.) - ✅ Inode tracking (handles in-place edits)
- ✅ Battle-tested in production
Automatic Photon Hot Reload
When you save a .photon.ts file:
1. File saved → PhotonWatcher detects change
2. File reloaded → New version compiled
3. Index updated → Tool schemas refreshed
4. Available immediately → Next `find` includes new versionExample:
# Edit your photon
vim ~/.ncp/photons/my-tool.photon.ts
# Save
# ↓ PhotonWatcher detects immediately
# ↓ NCP reloads
# ↓ Changes available
# Use immediately
ncp find "my tool" # ← Latest versionNo Manual Reload Needed
Before 2.3.0:
Edit photon → Restart NCP → Tools availableAfter 2.3.0:
Edit photon → Auto-detected → Tools available instantlyGraceful Fallback
When Daemon is Running
const broker = getBroker();
// ↓ Returns DaemonBroker
// ↓ Events route cross-process via Unix socket
// ↓ Instant delivery, zero latencyWhen Daemon is Not Running
const broker = getBroker();
// ↓ Returns NoOpBroker
// ↓ Events stay local (same process)
// ↓ Zero overhead, API identicalKey point: No configuration needed. Same code works both ways.
Best Practices
✅ Event Naming
// Good - descriptive, scoped
await this.emit({ type: 'data_fetched', data: {...} });
await this.emit({ type: 'file_updated', data: {...} });
await this.emit({ type: 'error_occurred', data: {...} });
// Bad - too generic
await this.emit({ type: 'event', data: {...} });
await this.emit({ type: 'notification', data: {...} });✅ Subscribe Only What You Need
// Good - specific subscriptions
/**
* @notify-on data_fetched,file_updated
*/
export default class MyPhoton extends Photon {
async onNotification(eventType, payload) {
// Handle only what you declared
}
}
// Avoid - subscribing to everything
/**
* @notify-on *
*/
// ↑ Don't do this - wastes resources✅ Error Handling in Events
async onNotification(eventType, payload) {
try {
if (eventType === 'data_ready') {
// Handle event
await processData(payload);
}
} catch (error) {
console.error(`Event handler error: ${error.message}`);
// Don't rethrow - prevent cascade failures
}
}❌ Synchronous Dependencies
// Bad - Photon A waits for Photon B's event
// This can deadlock or timeout
// Good - Photons are independent
// Photon A emits event, continues
// Photon B reacts asynchronously
// No coupling❌ Large Event Payloads
// Bad - huge object
await this.emit({
type: 'data_ready',
data: { allRecords: [/* 100,000 items */] }
});
// Good - send references or summaries
await this.emit({
type: 'data_ready',
data: {
recordCount: 100000,
summaryUrl: 'where to fetch data'
}
});Troubleshooting
Events not being delivered
Problem: Photon B doesn't receive event from Photon A
Check:
- Is Photon B declaring
@notify-onannotation? - Does event type match subscription?
- Is daemon running for cross-process events?
Solution:
// Photon B must declare subscription
/**
* @notify-on data_ready // ← Must match event type
*/
export default class MyPhoton extends Photon {
async onNotification(eventType, payload) {
// This is called when event emitted
}
}Photon hotload not working
Problem: Changed .photon.ts file but changes don't appear
Check:
- Did you save the file?
- Is PhotonWatcher running (in NCP logs)?
- Check file permissions
Solution:
# Force refresh
ncp list # Reloads all photons
# Or restart NCP
ncp --version # Triggers loadDaemon not using DaemonBroker
Problem: Events staying local even with daemon running
Check:
- Is daemon actually running?
- Are you starting daemon before NCP?
Solution:
# Terminal 1: Start daemon FIRST
photon daemon
# Terminal 2: Then start NCP (it will find daemon)
ncp find "..."Performance Notes
Event Routing Speed
Local events (NoOpBroker): < 1ms Cross-process events (DaemonBroker): < 5ms (via Unix socket)
Memory Usage
Events are ephemeral:
- Not persisted (unless you persist in Photon state)
- No accumulation
- Zero overhead when no subscriptions
Scalability
- Supports unlimited Photons
- Multiple subscribers per event type
- Daemon handles routing at scale
See Also
- Code-to-Photon - Workflow automation
- Stateful Execution - State persistence
- Photon Runtime - Custom MCPs
- NCP Ecosystem Roadmap - Full architecture