feat(gateway): add interrupt preemption telemetry and requester notice
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ?? '');
|
||||
|
||||
Reference in New Issue
Block a user