feat(session): persist model tier overrides per session
Store per-session config in SQLite and route /model and /reset through command fast-paths so channel sessions keep independent model selection across reconnects and restarts.
This commit is contained in:
+48
-27
@@ -7,6 +7,8 @@ import type {
|
||||
ToolSuccessEvent,
|
||||
ToolErrorEvent,
|
||||
ToolDeniedEvent,
|
||||
SkillsInstallerExecutionBlockedEvent,
|
||||
SkillsInstallerCommandResultEvent,
|
||||
SessionCreateEvent,
|
||||
SessionMessageEvent,
|
||||
SessionDeleteEvent,
|
||||
@@ -30,7 +32,7 @@ export class AuditLogger {
|
||||
constructor(config: AuditConfig) {
|
||||
this.config = config;
|
||||
this.rotator = new AuditRotator(config);
|
||||
|
||||
|
||||
if (!this.config.enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +55,7 @@ export class AuditLogger {
|
||||
}
|
||||
|
||||
this.rotator.checkRotation();
|
||||
|
||||
|
||||
const fullEvent: AuditEvent = { ...event, timestamp: Date.now() };
|
||||
this.writeStream!.write(JSON.stringify(fullEvent) + '\n');
|
||||
}
|
||||
@@ -67,49 +69,68 @@ export class AuditLogger {
|
||||
// ── Tool Events ───────────────────────────────────────────────
|
||||
|
||||
toolStart(event: ToolStartEvent): void {
|
||||
if (!this.shouldLog('tools', 'debug')) return;
|
||||
if (!this.shouldLog('tools', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'tool.start', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolSuccess(event: ToolSuccessEvent): void {
|
||||
if (!this.shouldLog('tools', 'debug')) return;
|
||||
if (!this.shouldLog('tools', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'tool.success', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolError(event: ToolErrorEvent): void {
|
||||
if (!this.shouldLog('tools', 'error')) return;
|
||||
if (!this.shouldLog('tools', 'error')) {return;}
|
||||
this.write({ level: 'error', event_type: 'tool.error', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolDenied(event: ToolDeniedEvent): void {
|
||||
if (!this.shouldLog('tools', 'warn')) return;
|
||||
if (!this.shouldLog('tools', 'warn')) {return;}
|
||||
this.write({ level: 'warn', event_type: 'tool.denied', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
skillsInstallerExecutionBlocked(event: SkillsInstallerExecutionBlockedEvent): void {
|
||||
if (!this.shouldLog('tools', 'warn')) {return;}
|
||||
this.write({
|
||||
level: 'warn',
|
||||
event_type: 'skills.installer.execution_blocked',
|
||||
event: event as unknown as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
skillsInstallerCommandResult(event: SkillsInstallerCommandResultEvent): void {
|
||||
const level = event.status === 'succeeded' ? 'debug' : event.reason === 'allowlist_blocked' ? 'warn' : 'error';
|
||||
if (!this.shouldLog('tools', level)) {return;}
|
||||
this.write({
|
||||
level,
|
||||
event_type: 'skills.installer.command_result',
|
||||
event: event as unknown as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Session Events ───────────────────────────────────────────
|
||||
|
||||
sessionCreate(event: SessionCreateEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'session.create', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionMessage(event: SessionMessageEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'session.message', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionDelete(event: SessionDeleteEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'session.delete', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionCompact(event: SessionCompactEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'session.compact', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionTransfer(from: string, to: string, messageCount: number): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
if (!this.shouldLog('sessions', 'debug')) {return;}
|
||||
this.write({
|
||||
level: 'debug',
|
||||
event_type: 'session.transfer',
|
||||
@@ -121,12 +142,12 @@ export class AuditLogger {
|
||||
|
||||
// Cron
|
||||
cronTrigger(event: CronTriggerEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'cron.trigger', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
cronAdd(jobName: string, schedule: string): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
if (!this.shouldLog('automation', 'info')) {return;}
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'cron.add',
|
||||
@@ -135,7 +156,7 @@ export class AuditLogger {
|
||||
}
|
||||
|
||||
cronRemove(jobName: string): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
if (!this.shouldLog('automation', 'info')) {return;}
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'cron.remove',
|
||||
@@ -145,12 +166,12 @@ export class AuditLogger {
|
||||
|
||||
// Webhook
|
||||
webhookReceive(event: WebhookReceiveEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'webhook.receive', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
webhookNotFound(webhookName: string): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
if (!this.shouldLog('automation', 'warn')) {return;}
|
||||
this.write({
|
||||
level: 'warn',
|
||||
event_type: 'webhook.not_found',
|
||||
@@ -159,7 +180,7 @@ export class AuditLogger {
|
||||
}
|
||||
|
||||
webhookDenied(webhookName: string, reason: string): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
if (!this.shouldLog('automation', 'warn')) {return;}
|
||||
this.write({
|
||||
level: 'warn',
|
||||
event_type: 'webhook.denied',
|
||||
@@ -169,38 +190,38 @@ export class AuditLogger {
|
||||
|
||||
// Heartbeat
|
||||
heartbeatCycle(event: HeartbeatCycleEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'heartbeat.cycle', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatCheck(event: HeartbeatCheckEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'heartbeat.check', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatFail(event: HeartbeatFailEvent): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
if (!this.shouldLog('automation', 'warn')) {return;}
|
||||
this.write({ level: 'warn', event_type: 'heartbeat.fail', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatRecover(event: HeartbeatRecoverEvent): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
if (!this.shouldLog('automation', 'info')) {return;}
|
||||
this.write({ level: 'info', event_type: 'heartbeat.recover', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
// Gmail
|
||||
gmailPoll(event: GmailPollEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'gmail.poll', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
gmailNewEmail(event: GmailNewEmailEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
if (!this.shouldLog('automation', 'debug')) {return;}
|
||||
this.write({ level: 'debug', event_type: 'gmail.new_email', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
gmailError(error: string, context?: string): void {
|
||||
if (!this.shouldLog('automation', 'error')) return;
|
||||
if (!this.shouldLog('automation', 'error')) {return;}
|
||||
this.write({
|
||||
level: 'error',
|
||||
event_type: 'gmail.error',
|
||||
@@ -211,7 +232,7 @@ export class AuditLogger {
|
||||
// ── System Events ────────────────────────────────────────────
|
||||
|
||||
systemStart(component: string, config?: Record<string, unknown>): void {
|
||||
if (!this.config.enabled) return;
|
||||
if (!this.config.enabled) {return;}
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.start',
|
||||
@@ -220,7 +241,7 @@ export class AuditLogger {
|
||||
}
|
||||
|
||||
systemStop(component: string, reason?: string): void {
|
||||
if (!this.config.enabled) return;
|
||||
if (!this.config.enabled) {return;}
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.stop',
|
||||
@@ -229,7 +250,7 @@ export class AuditLogger {
|
||||
}
|
||||
|
||||
systemConfig(component: string, action: string, config: Record<string, unknown>): void {
|
||||
if (!this.config.enabled) return;
|
||||
if (!this.config.enabled) {return;}
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.config',
|
||||
|
||||
@@ -55,14 +55,14 @@ export class AuditRotator {
|
||||
if (existsSync(compressedPath)) {
|
||||
fs.unlink(compressedPath);
|
||||
}
|
||||
|
||||
|
||||
fs.rename(basePath, rotatedPath);
|
||||
|
||||
// Compress the rotated file
|
||||
const gzip = createGzip();
|
||||
const input = createReadStream(rotatedPath);
|
||||
const output = createWriteStream(compressedPath);
|
||||
|
||||
|
||||
pipeline(input, gzip, output).then(() => {
|
||||
fs.unlink(rotatedPath);
|
||||
}).catch((err) => {
|
||||
@@ -84,9 +84,9 @@ export class AuditRotator {
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.startsWith(baseName)) continue;
|
||||
if (!file.startsWith(baseName)) {continue;}
|
||||
|
||||
const filePath = `${dir}/${file}`;
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
@@ -3,6 +3,8 @@ export type AuditLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type AuditEventType =
|
||||
// Tool execution
|
||||
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied'
|
||||
// Skills installer
|
||||
| 'skills.installer.execution_blocked' | 'skills.installer.command_result'
|
||||
// Session lifecycle
|
||||
| 'session.create' | 'session.message' | 'session.delete' | 'session.transfer' | 'session.compact'
|
||||
// Automation - Cron
|
||||
@@ -75,6 +77,24 @@ export interface ToolDeniedEvent {
|
||||
denial_type: 'policy' | 'hook' | 'not_found' | 'autonomy_override';
|
||||
}
|
||||
|
||||
export interface SkillsInstallerExecutionBlockedEvent {
|
||||
skill_name: string;
|
||||
phase: 'install' | 'execute';
|
||||
execution_requested: boolean;
|
||||
execution_enabled: boolean;
|
||||
reason: string;
|
||||
attempted_command_count: number;
|
||||
}
|
||||
|
||||
export interface SkillsInstallerCommandResultEvent {
|
||||
skill_name: string;
|
||||
phase: 'install' | 'execute';
|
||||
installer_type: string;
|
||||
command: string;
|
||||
status: 'blocked' | 'skipped' | 'succeeded' | 'failed';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SessionCreateEvent {
|
||||
session_id: string;
|
||||
frontend: string;
|
||||
|
||||
Reference in New Issue
Block a user