Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions src/commands/ai/generate/server/AIGenerateServerCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export class AIGenerateServerCommand extends AIGenerateCommand {
// Mode selection: RAG context building OR direct messages
if (params.roomId) {
// RAG MODE: Build context from chat room (SAME code path as PersonaUser)
console.log(`🤖 AI Generate: Building RAG context for room ${params.roomId.slice(0, 8)}...`);

// Find persona if not specified
let targetPersonaId = params.personaId;
Expand All @@ -57,7 +56,6 @@ export class AIGenerateServerCommand extends AIGenerateCommand {
const personaRecord = usersResult.data[0];
targetPersonaId = personaRecord.id;
personaDisplayName = personaRecord.data.displayName;
console.log(`✅ AI Generate: Using persona "${personaRecord.data.displayName}" (${targetPersonaId.slice(0, 8)})`);
}

// Build RAG context (SAME code as PersonaUser.respondToMessage line 207-215)
Expand Down Expand Up @@ -134,7 +132,6 @@ export class AIGenerateServerCommand extends AIGenerateCommand {

} else if (params.messages) {
// DIRECT MODE: Use provided messages
console.log(`🤖 AI Generate: Using provided messages (${params.messages.length})...`);
request = paramsToRequest(params);

} else {
Expand All @@ -143,7 +140,6 @@ export class AIGenerateServerCommand extends AIGenerateCommand {

// PREVIEW MODE: Return request without calling LLM
if (params.preview) {
console.log(`👁️ AI Generate: Preview mode - returning request without LLM call`);
const formatted = this.formatRequestPreview(request, ragContext);

return createAIGenerateResultFromParams(params, {
Expand All @@ -156,15 +152,9 @@ export class AIGenerateServerCommand extends AIGenerateCommand {
}

// GENERATION MODE: Call AIProviderDaemon
console.log(`🤖 AI Generate: Calling LLM with ${request.messages.length} messages...`);
const response = await AIProviderDaemon.generateText(request);

const result = responseToResult(response, params);
console.log(`✅ AI Generate: Generated ${result.usage?.outputTokens} tokens in ${result.responseTimeMs}ms`);

return result;
return responseToResult(response, params);
} catch (error) {
console.error(`❌ AI Generate: Execution failed:`, error);
return createErrorResult(params, error instanceof Error ? error.message : String(error));
}
}
Expand Down
31 changes: 29 additions & 2 deletions src/daemons/ai-provider-daemon/server/AdapterHealthMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ export class AdapterHealthMonitor {
await Promise.allSettled(checks);
}

/**
* Compute per-adapter check interval with exponential backoff.
* Healthy adapters use base interval. Failed adapters back off exponentially
* up to a ceiling, so they still recover when network returns.
*
* Backoff schedule (with 30s base):
* 0 failures → 30s
* 1 failure → 60s
* 2 failures → 120s
* 3 failures → 240s (4 min)
* 5+ failures → 300s (5 min ceiling)
*/
private adapterCheckInterval(state: AdapterHealthState, baseInterval: number): number {
if (state.consecutiveFailures === 0) return baseInterval;
const backoff = baseInterval * Math.pow(2, state.consecutiveFailures);
const ceiling = 5 * 60 * 1000; // 5-minute max — ensures recovery from transient network issues
return Math.min(backoff, ceiling);
}

/**
* Check a single adapter's health
* Concurrent-safe with per-adapter lock
Expand All @@ -188,8 +207,11 @@ export class AdapterHealthMonitor {
return;
}

// Exponential backoff for failing adapters (still retries at ceiling for recovery)
const effectiveInterval = this.adapterCheckInterval(state, checkInterval);

// Check if enough time has passed since last check
if (now - state.lastCheckTime < checkInterval) {
if (now - state.lastCheckTime < effectiveInterval) {
return; // Too soon for this adapter
}

Expand All @@ -211,7 +233,12 @@ export class AdapterHealthMonitor {
state.consecutiveFailures = 0;
} else {
state.consecutiveFailures++;
log.warn(`⚠️ ${state.adapter.providerId}: Health check failed (${state.consecutiveFailures} consecutive failures)`);
const nextRetryMs = this.adapterCheckInterval(state, checkInterval);
const nextRetrySec = Math.round(nextRetryMs / 1000);
// Only log on first failure and at backoff milestones (powers of 2) to reduce noise
if (state.consecutiveFailures === 1 || (state.consecutiveFailures & (state.consecutiveFailures - 1)) === 0) {
log.warn(`⚠️ ${state.adapter.providerId}: Health check failed (${state.consecutiveFailures} consecutive, next retry in ${nextRetrySec}s)`);
}

// Get max failures threshold from SystemDaemon
const systemDaemon = SystemDaemon.sharedInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export abstract class BaseOpenAICompatibleAdapter extends BaseAIProviderAdapter
protected readonly config: OpenAICompatibleConfig;
protected isInitialized = false;

// Throttle per-status log messages (avoid spamming same error every call)
private _lastStatusLogTime: Map<string, number> = new Map();
private readonly _statusLogThrottleMs = 5 * 60 * 1000; // 5 minutes

constructor(config: OpenAICompatibleConfig) {
super();
this.config = config;
Expand Down Expand Up @@ -731,7 +735,13 @@ export abstract class BaseOpenAICompatibleAdapter extends BaseAIProviderAdapter
timestamp: Date.now(),
});

this.log(null, 'error', `💰 ${this.providerName}: ${status} - ${errorBody.slice(0, 200)}`);
// Throttle log to once per 5 minutes per status (avoid spamming same error)
const now = Date.now();
const lastLog = this._lastStatusLogTime.get(status) ?? 0;
if (now - lastLog >= this._statusLogThrottleMs) {
this._lastStatusLogTime.set(status, now);
this.log(null, 'error', `💰 ${this.providerName}: ${status} - ${errorBody.slice(0, 200)}`);
}
}

throw new Error(`HTTP ${response.status}: ${errorBody}`);
Expand Down
2 changes: 0 additions & 2 deletions src/daemons/data-daemon/server/DatabaseHandleRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ export class DatabaseHandleRegistry {

// Initialize default handle metadata
const expandedDbPath = getDatabasePath();
console.log(`📦 DatabaseHandleRegistry: Path registry initialized (default db: ${expandedDbPath})`);

this.handleMetadata.set(DEFAULT_HANDLE, {
adapter: 'rust' as AdapterType, // All I/O goes through Rust
Expand Down Expand Up @@ -218,7 +217,6 @@ export class DatabaseHandleRegistry {
throw new Error('SQLite config requires either "path" or "filename" property');
}
// Just register the path - Rust handles actual connections
console.log(`📦 DatabaseHandleRegistry: Registered handle ${handle.substring(0, 8)}... → ${dbPath}`);
break;
}

Expand Down
5 changes: 2 additions & 3 deletions src/daemons/data-daemon/server/ORMRustClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ export class ORMRustClient {
this.socket.on('connect', () => {
this.connected = true;
this.connecting = false;
console.log('[ORMRustClient] Connected to continuum-core');
resolve();
});

Expand Down Expand Up @@ -223,8 +222,8 @@ export class ORMRustClient {
const networkAndRustMs = totalMs - timing.stringifyMs - timing.writeMs - parseMs;
this.pendingTimings.delete(response.requestId);

// Log slow operations (>50ms threshold matches Rust)
if (totalMs > 50) {
// Log slow operations (>1000ms — raised from 50ms to reduce startup noise)
if (totalMs > 1000) {
console.warn(`[ORMRustClient] SLOW IPC: ${timing.command} total=${totalMs}ms (stringify=${timing.stringifyMs}ms write=${timing.writeMs}ms network+rust=${networkAndRustMs}ms parse=${parseMs}ms)`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/daemons/data-daemon/shared/ORMLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export function logOperationStart(
console.log(`[ORM] ${operation} ${collection} completed in ${durationMs}ms`);
}

// Warn on slow operations
if (durationMs > 100) {
// Warn on slow operations (>1000ms — raised from 100ms to reduce startup noise)
if (durationMs > 1000) {
console.warn(`[ORM] SLOW: ${operation} ${collection} took ${durationMs}ms`);
}
};
Expand Down
80 changes: 59 additions & 21 deletions src/daemons/events-daemon/browser/EventsDaemonBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ export class EventsDaemonBrowser extends EventsDaemon implements IEventSubscript
private domEventBridge: DOMEventBridge;
private subscriptionManager = new EventSubscriptionManager();

/**
* Registry of event names with active DOM listeners.
* DOM CustomEvent dispatch is skipped for events not in this set.
* Widgets register via registerDOMInterest() when they need document-level events.
*/
private static _domInterest = new Set<string>();

constructor(context: JTAGContext, router: JTAGRouter) {
super(context, router);

// Reduce log spam - debug logs removed
// console.log(`🔥 CLAUDE-BROWSER-DAEMON-DEBUG-${Date.now()}: EventsDaemonBrowser constructor called!`);
// console.log(`🔥 Context: ${context.environment}/${context.uuid}`);
// console.log(`🔥 ENDPOINT-DEBUG: EventsDaemonBrowser.subpath = "${this.subpath}"`);
// console.log(`🔥 ENDPOINT-DEBUG: Expected browser endpoint should be "browser/${this.subpath}"`);

// Setup DOM event bridge for widget communication
this.domEventBridge = new DOMEventBridge(this.eventManager);
verbose() && console.log('🌉 EventsDaemonBrowser: DOM event bridge initialized');
Expand All @@ -56,33 +57,70 @@ export class EventsDaemonBrowser extends EventsDaemon implements IEventSubscript
}

/**
* Handle local event bridging - emit to event system AND DOM for BaseWidget
* Register interest in receiving DOM CustomEvents for a specific event name.
* Only events with registered interest will be dispatched to the document.
* Returns an unregister function.
*/
protected handleLocalEventBridge(eventName: string, eventData: unknown): void {
// 1. Emit to local event system - DOMEventBridge will automatically handle DOM dispatch
this.eventManager.events.emit(eventName, eventData);
public static registerDOMInterest(eventName: string): () => void {
EventsDaemonBrowser._domInterest.add(eventName);
return () => {
EventsDaemonBrowser._domInterest.delete(eventName);
};
Comment on lines +64 to +68
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerDOMInterest() uses a Set and the returned unregister function unconditionally deletes the event name. If multiple widgets/services register the same event, the first cleanup will delete interest for all and can prevent DOM dispatch for remaining listeners. Consider ref-counting interests (Map<string, number>) so unregister only removes when the last registrant cleans up.

Copilot uses AI. Check for mistakes.
}

// 2. Dispatch DOM event for BaseWidget integration (backward compatibility)
const domEvent = new CustomEvent(eventName, {
detail: eventData
});
/**
* Check if anything has registered DOM interest for this event name.
* Checks both:
* - Events.domInterest (populated by Events.subscribe() in browser)
* - _domInterest (populated by registerDOMInterest() from BaseWidget/WidgetEventServiceBrowser)
* Uses prefix matching: 'data:chat_messages' matches 'data:chat_messages:created'.
*/
private hasDOMInterest(eventName: string): boolean {
// Direct match in either registry
if (Events.domInterest.has(eventName)) return true;
if (EventsDaemonBrowser._domInterest.has(eventName)) return true;

// Type-safe document access for browser environment
if (typeof globalThis !== 'undefined' && 'document' in globalThis) {
(globalThis as typeof globalThis & { document: Document }).document.dispatchEvent(domEvent);
// Prefix match against both registries
for (const interest of Events.domInterest) {
if (eventName.startsWith(interest + ':') || interest.startsWith(eventName + ':')) return true;
}
for (const interest of EventsDaemonBrowser._domInterest) {
if (eventName.startsWith(interest + ':') || interest.startsWith(eventName + ':')) return true;
}
return false;
}

/**
* Handle local event bridging - emit to event system AND DOM for BaseWidget
*
* Dispatch order:
* 1. Internal EventEmitter (DOMEventBridge handles mapped events)
* 2. SubscriptionManager (exact, wildcard, elegant pattern matching)
* 3. Legacy wildcard subscriptions
* 4. DOM CustomEvent ONLY if a widget registered interest (filter-first, not spam-then-filter)
*/
protected handleLocalEventBridge(eventName: string, eventData: unknown): void {
// 1. Emit to local event system — DOMEventBridge handles its mapped events
this.eventManager.events.emit(eventName, eventData);

// 3. Trigger unified subscription manager (NEW!)
// This handles exact, wildcard, and elegant pattern subscriptions
// 2. Trigger unified subscription manager (exact, wildcard, and elegant patterns)
this.subscriptionManager.trigger(eventName, eventData);

// 4. Legacy: Also check wildcard subscriptions from Events.subscribe()
// TODO: Migrate to unified subscription manager
// 3. Legacy: Also check wildcard subscriptions from Events.subscribe()
try {
Events.checkWildcardSubscriptions(eventName, eventData);
} catch (error) {
console.error('Failed to check wildcard subscriptions:', error);
}

// 4. DOM dispatch — ONLY if a widget registered interest for this event namespace
// This prevents creating DOM CustomEvents for high-frequency events no widget cares about
if (this.hasDOMInterest(eventName)) {
if (typeof globalThis !== 'undefined' && 'document' in globalThis) {
const domEvent = new CustomEvent(eventName, { detail: eventData });
(globalThis as typeof globalThis & { document: Document }).document.dispatchEvent(domEvent);
}
}
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/daemons/events-daemon/shared/EventsDaemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { DaemonBase } from '../../command-daemon/shared/DaemonBase';
class EventRateLimiter {
private counts = new Map<string, number>();
private windowStart = Date.now();
private readonly windowMs = 100; // 100ms window
private readonly maxPerWindow = 20; // Max 20 of same event per window
private readonly warnThreshold = 10; // Warn at 10+ per window
private readonly windowMs = 1000; // 1-second window (matches Rust-side rate limiter)
private readonly maxPerWindow = 200; // Max 200 of same event per second
private readonly warnThreshold = 100; // Warn at 100+ per second
private blocked = new Set<string>();
private warned = new Set<string>(); // Track warned events to avoid spam

Expand All @@ -40,7 +40,7 @@ class EventRateLimiter {
.sort((a, b) => b[1] - a[1]);

if (hotEvents.length > 0) {
console.warn(`⚠️ EVENT ACTIVITY: ${hotEvents.map(([e, c]) => `${e}(${c})`).join(', ')}`);
console.warn(`[EventRateLimiter] EVENT ACTIVITY: ${hotEvents.map(([e, c]) => `${e}(${c})`).join(', ')}`);
}
}

Expand All @@ -63,7 +63,7 @@ class EventRateLimiter {
if (count === this.warnThreshold && !this.warned.has(eventName)) {
this.warned.add(eventName);
this.totalWarned++;
console.warn(`⚠️ EVENT TRENDING: "${eventName}" at ${count}x in ${this.windowMs}ms (blocking at ${this.maxPerWindow})`);
console.warn(`[EventRateLimiter] EVENT TRENDING: "${eventName}" at ${count}x in ${this.windowMs}ms (blocking at ${this.maxPerWindow})`);
}

// Block if over threshold
Expand All @@ -75,7 +75,7 @@ class EventRateLimiter {
if (this.blockedHistory.length > 100) {
this.blockedHistory.shift();
}
console.error(`🛑 EVENT CASCADE BLOCKED: "${eventName}" fired ${count}x in ${this.windowMs}ms`);
console.error(`[EventRateLimiter] EVENT CASCADE BLOCKED: "${eventName}" fired ${count}x in ${this.windowMs}ms`);
return true;
}

Expand Down
Loading
Loading