feat(gateway): add interrupt preemption telemetry and requester notice

This commit is contained in:
William Valentin
2026-02-19 11:48:41 -08:00
parent 6b56d9e223
commit 01cd726d7c
8 changed files with 82 additions and 11 deletions
+6
View File
@@ -19,6 +19,7 @@ import type {
SessionCheckpointEvent,
SessionAutoCompactEvent,
UserActionEvent,
QueuePreemptEvent,
BackendRouteEvent,
BackendFallbackEvent,
CronTriggerEvent,
@@ -194,6 +195,11 @@ export class AuditLogger {
this.write({ level: 'info', event_type: 'user.action', event: event as unknown as Record<string, unknown> });
}
queuePreempt(event: QueuePreemptEvent): void {
if (!this.shouldLog('sessions', 'info')) {return;}
this.write({ level: 'info', event_type: 'queue.preempt', 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> });
+11
View File
@@ -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'
| 'queue.preempt'
| 'backend.route' | 'backend.fallback'
// Automation - Cron
| 'cron.trigger' | 'cron.sent' | 'cron.add' | 'cron.remove'
@@ -210,6 +211,16 @@ export interface UserActionEvent {
command?: string;
}
export interface QueuePreemptEvent {
session_id: string;
channel: string;
sender: string;
lane_id: string;
request_id: string;
mode: 'interrupt';
cancelled_active_run: boolean;
}
export interface BackendRouteEvent {
session_id: string;
channel: string;
+21 -1
View File
@@ -49,6 +49,7 @@ describe('createAgentHandlers command fast-path', () => {
registerBuiltinCommands(commandRegistry);
const mockAuditLogger = {
userAction: vi.fn(),
queuePreempt: vi.fn(),
};
const handlers = createAgentHandlers({
@@ -364,6 +365,16 @@ describe('createAgentHandlers command fast-path', () => {
});
describe('createAgentHandlers queue policy resolution', () => {
const mockAuditLogger = {
userAction: vi.fn(),
queuePreempt: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
initAuditLogger(mockAuditLogger as any);
});
it('passes resolved per-request queue policy into lane enqueue', async () => {
const mockAgent = {
process: vi.fn(async () => 'ok'),
@@ -554,6 +565,15 @@ describe('createAgentHandlers queue policy resolution', () => {
expect(sessionBridge.cancelSession).toHaveBeenCalledWith('ws:s1');
expect(sessionBridge.cancel).not.toHaveBeenCalled();
expect((sent[0] as GatewayEvent).event).toBe('done');
expect(mockAuditLogger.queuePreempt).toHaveBeenCalledWith(expect.objectContaining({
session_id: 'ws:s1',
lane_id: 'ws:s1',
request_id: '7',
mode: 'interrupt',
cancelled_active_run: true,
}));
expect((sent[0] as GatewayEvent).event).toBe('content');
expect(((sent[0] as GatewayEvent).data as { text: string }).text).toContain('Interrupt mode');
expect((sent[1] as GatewayEvent).event).toBe('done');
});
});
+21 -6
View File
@@ -90,26 +90,41 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
const laneIsProcessing = typeof laneQueueWithProcessing.isProcessing === 'function'
? laneQueueWithProcessing.isProcessing(laneId)
: false;
const requestId = request.id.toString();
let interruptedPreviousRun = false;
// Interrupt mode should preempt active work when a newer request arrives.
// LaneQueue itself only rejects queued entries, so we also request agent cancellation.
if (resolvedPolicy?.mode === 'interrupt' && laneIsProcessing) {
if (sessionId) {
deps.sessionBridge.cancelSession(sessionId);
} else {
deps.sessionBridge.cancel(connectionId);
}
const cancelled = sessionId
? deps.sessionBridge.cancelSession(sessionId)
: deps.sessionBridge.cancel(connectionId);
interruptedPreviousRun = cancelled;
auditLogger?.queuePreempt?.({
session_id: sessionId ?? `ws:${connectionId}`,
channel: 'ws',
sender: connectionId,
lane_id: laneId,
request_id: requestId,
mode: 'interrupt',
cancelled_active_run: cancelled,
});
}
// Enqueue the work — if the lane is idle it runs immediately,
// otherwise it waits for earlier requests on the same session to finish.
const requestId = request.id.toString();
deps.metrics?.startRequest(requestId, { sessionId: laneId, channel: 'ws' });
try {
return await deps.laneQueue.enqueue(laneId, async () => {
deps.sessionBridge.setBusy(connectionId, true);
if (interruptedPreviousRun) {
await send(makeEvent(request.id, 'content', {
text: 'Interrupt mode: cancelled the previous in-flight run and processing your latest message.',
}));
}
const commandInput = safeParams.metadata?.isCommand && typeof safeParams.metadata.command === 'string'
? `/${safeParams.metadata.command}${safeParams.metadata.commandArgs ? ` ${safeParams.metadata.commandArgs}` : ''}`
: (safeParams.message ?? '');