feat(audit): log backend routing decisions and fallback events
This commit is contained in:
@@ -19,6 +19,8 @@ import type {
|
|||||||
SessionCheckpointEvent,
|
SessionCheckpointEvent,
|
||||||
SessionAutoCompactEvent,
|
SessionAutoCompactEvent,
|
||||||
UserActionEvent,
|
UserActionEvent,
|
||||||
|
BackendRouteEvent,
|
||||||
|
BackendFallbackEvent,
|
||||||
CronTriggerEvent,
|
CronTriggerEvent,
|
||||||
WebhookReceiveEvent,
|
WebhookReceiveEvent,
|
||||||
HeartbeatCycleEvent,
|
HeartbeatCycleEvent,
|
||||||
@@ -192,6 +194,16 @@ export class AuditLogger {
|
|||||||
this.write({ level: 'info', event_type: 'user.action', event: event as unknown as Record<string, unknown> });
|
this.write({ level: 'info', event_type: 'user.action', event: event as unknown as Record<string, unknown> });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backendRoute(event: BackendRouteEvent): void {
|
||||||
|
if (!this.shouldLog('sessions', 'info')) {return;}
|
||||||
|
this.write({ level: 'info', event_type: 'backend.route', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
|
backendFallback(event: BackendFallbackEvent): void {
|
||||||
|
if (!this.shouldLog('sessions', 'warn')) {return;}
|
||||||
|
this.write({ level: 'warn', event_type: 'backend.fallback', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
sessionTransfer(from: string, to: string, messageCount: number): void {
|
sessionTransfer(from: string, to: string, messageCount: number): void {
|
||||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||||
this.write({
|
this.write({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type AuditEventType =
|
|||||||
| 'skills.installer.execution_blocked' | 'skills.installer.command_result' | 'skills.registry_install'
|
| 'skills.installer.execution_blocked' | 'skills.installer.command_result' | 'skills.registry_install'
|
||||||
// Session lifecycle
|
// Session lifecycle
|
||||||
| 'session.create' | 'session.message' | 'session.delete' | 'session.transfer' | 'session.compact' | 'session.checkpoint' | 'session.auto_compact' | 'user.action'
|
| 'session.create' | 'session.message' | 'session.delete' | 'session.transfer' | 'session.compact' | 'session.checkpoint' | 'session.auto_compact' | 'user.action'
|
||||||
|
| 'backend.route' | 'backend.fallback'
|
||||||
// Automation - Cron
|
// Automation - Cron
|
||||||
| 'cron.trigger' | 'cron.sent' | 'cron.add' | 'cron.remove'
|
| 'cron.trigger' | 'cron.sent' | 'cron.add' | 'cron.remove'
|
||||||
// Automation - Webhook
|
// Automation - Webhook
|
||||||
@@ -209,6 +210,23 @@ export interface UserActionEvent {
|
|||||||
command?: string;
|
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 {
|
export interface CronTriggerEvent {
|
||||||
job_name: string;
|
job_name: string;
|
||||||
schedule: string;
|
schedule: string;
|
||||||
|
|||||||
+28
-2
@@ -919,9 +919,25 @@ export function createMessageRouter(deps: {
|
|||||||
// buildUserMessage() in the agent will create native audio content parts
|
// buildUserMessage() in the agent will create native audio content parts
|
||||||
|
|
||||||
const requestedBackend = agentConfig?.backend ?? deps.defaultName;
|
const requestedBackend = agentConfig?.backend ?? deps.defaultName;
|
||||||
|
const sessionIdForAudit = `${msg.channel}:${msg.senderId}`;
|
||||||
const selectedBackend = requestedBackend && requestedBackend !== 'native'
|
const selectedBackend = requestedBackend && requestedBackend !== 'native'
|
||||||
? deps.externalBackends?.[requestedBackend]
|
? deps.externalBackends?.[requestedBackend]
|
||||||
: undefined;
|
: 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)) {
|
if (selectedBackend && (!attachments || attachments.length === 0)) {
|
||||||
try {
|
try {
|
||||||
@@ -935,8 +951,18 @@ export function createMessageRouter(deps: {
|
|||||||
await reply({ text: response, replyTo: msg.id });
|
await reply({ text: response, replyTo: msg.id });
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const detail = error instanceof Error ? error.message : String(error);
|
||||||
console.warn(`External backend "${selectedBackend.name}" failed, falling back to native: ${message}`);
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user