From 898828bb70c01f7c59600253bb94cd4b0abb9218 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 17 Feb 2026 09:50:44 -0800 Subject: [PATCH] feat(audit): log backend routing decisions and fallback events --- src/audit/logger.ts | 12 ++++++++++++ src/audit/types.ts | 18 ++++++++++++++++++ src/daemon/routing.ts | 30 ++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/audit/logger.ts b/src/audit/logger.ts index 78e2840..f69b1b7 100644 --- a/src/audit/logger.ts +++ b/src/audit/logger.ts @@ -19,6 +19,8 @@ import type { SessionCheckpointEvent, SessionAutoCompactEvent, UserActionEvent, + BackendRouteEvent, + BackendFallbackEvent, CronTriggerEvent, WebhookReceiveEvent, HeartbeatCycleEvent, @@ -192,6 +194,16 @@ export class AuditLogger { this.write({ level: 'info', event_type: 'user.action', event: event as unknown as Record }); } + backendRoute(event: BackendRouteEvent): void { + if (!this.shouldLog('sessions', 'info')) {return;} + this.write({ level: 'info', event_type: 'backend.route', event: event as unknown as Record }); + } + + backendFallback(event: BackendFallbackEvent): void { + if (!this.shouldLog('sessions', 'warn')) {return;} + this.write({ level: 'warn', event_type: 'backend.fallback', event: event as unknown as Record }); + } + sessionTransfer(from: string, to: string, messageCount: number): void { if (!this.shouldLog('sessions', 'debug')) {return;} this.write({ diff --git a/src/audit/types.ts b/src/audit/types.ts index 0ea55fc..69b9fcc 100644 --- a/src/audit/types.ts +++ b/src/audit/types.ts @@ -11,6 +11,7 @@ export type AuditEventType = | 'skills.installer.execution_blocked' | 'skills.installer.command_result' | 'skills.registry_install' // Session lifecycle | 'session.create' | 'session.message' | 'session.delete' | 'session.transfer' | 'session.compact' | 'session.checkpoint' | 'session.auto_compact' | 'user.action' + | 'backend.route' | 'backend.fallback' // Automation - Cron | 'cron.trigger' | 'cron.sent' | 'cron.add' | 'cron.remove' // Automation - Webhook @@ -209,6 +210,23 @@ export interface UserActionEvent { command?: string; } +export interface BackendRouteEvent { + session_id: string; + channel: string; + sender: string; + selected_backend: 'native' | 'claude_code' | 'opencode' | 'codex' | 'gemini'; + source: 'agent_override' | 'default_external' | 'native'; +} + +export interface BackendFallbackEvent { + session_id: string; + channel: string; + sender: string; + from_backend: 'claude_code' | 'opencode' | 'codex' | 'gemini'; + to_backend: 'native'; + reason: string; +} + export interface CronTriggerEvent { job_name: string; schedule: string; diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index a3a1725..2c0e906 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -919,9 +919,25 @@ export function createMessageRouter(deps: { // buildUserMessage() in the agent will create native audio content parts const requestedBackend = agentConfig?.backend ?? deps.defaultName; + const sessionIdForAudit = `${msg.channel}:${msg.senderId}`; const selectedBackend = requestedBackend && requestedBackend !== 'native' ? deps.externalBackends?.[requestedBackend] : undefined; + const selectedBackendForAudit: 'native' | ExternalBackendName = selectedBackend && requestedBackend + ? requestedBackend + : 'native'; + + auditLogger?.backendRoute?.({ + session_id: sessionIdForAudit, + channel: msg.channel, + sender: msg.senderId, + selected_backend: selectedBackendForAudit, + source: agentConfig?.backend + ? 'agent_override' + : selectedBackend + ? 'default_external' + : 'native', + }); if (selectedBackend && (!attachments || attachments.length === 0)) { try { @@ -935,8 +951,18 @@ export function createMessageRouter(deps: { await reply({ text: response, replyTo: msg.id }); return; } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`External backend "${selectedBackend.name}" failed, falling back to native: ${message}`); + const detail = error instanceof Error ? error.message : String(error); + console.warn(`External backend "${selectedBackend.name}" failed, falling back to native: ${detail}`); + auditLogger?.backendFallback?.({ + session_id: sessionIdForAudit, + channel: msg.channel, + sender: msg.senderId, + from_backend: (requestedBackend && requestedBackend !== 'native') + ? requestedBackend + : (selectedBackend.name as ExternalBackendName), + to_backend: 'native', + reason: detail, + }); } }