feat: preempt active runs in interrupt queue mode

This commit is contained in:
William Valentin
2026-02-18 10:06:35 -08:00
parent b786b1435a
commit 9cbd66cdcc
7 changed files with 145 additions and 5 deletions
+60
View File
@@ -367,4 +367,64 @@ describe('createAgentHandlers queue policy resolution', () => {
expect((event.data as { code: number }).code).toBe(3);
expect((event.data as { queue?: { code: string } }).queue?.code).toBe('overflow');
});
it('requests active-session cancellation when interrupt mode receives a new message', async () => {
const mockAgent = {
process: vi.fn(async () => 'ok'),
consumeContextAlert: vi.fn(() => undefined),
getContextBudget: vi.fn(() => ({
estimatedTokens: 0,
contextWindow: 128000,
remainingTokens: 128000,
usagePct: 0,
thresholdPct: 80,
thresholdTokens: 102400,
shouldCompact: false,
})),
getUsage: vi.fn(() => ({
primary: { inputTokens: 0, outputTokens: 0, calls: 0 },
delegation: {},
total: { inputTokens: 0, outputTokens: 0, calls: 0, estimatedCost: 0 },
})),
getModelTier: vi.fn(() => 'default'),
setModelTier: vi.fn(),
compact: vi.fn(async () => null),
reset: vi.fn(),
};
const sessionBridge = {
getAgent: vi.fn(() => mockAgent),
getSessionId: vi.fn(() => 'ws:s1'),
setBusy: vi.fn(),
setOnToolUse: vi.fn(),
isBusy: vi.fn(() => false),
cancelSession: vi.fn(() => true),
cancel: vi.fn(() => true),
};
const laneQueue = {
enqueue: vi.fn(async (_laneId: string, work: () => Promise<unknown>) => work()),
cancel: vi.fn(),
isProcessing: vi.fn(() => true),
} as unknown as LaneQueue;
const handlers = createAgentHandlers({
sessionBridge: sessionBridge as unknown as AgentHandlerDeps['sessionBridge'],
laneQueue,
resolveQueuePolicy: vi.fn(() => ({ mode: 'interrupt' as const })),
});
const sent: OutboundMessage[] = [];
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
await handlers['agent.send']({
id: 7,
method: 'agent.send',
params: { message: 'newest', connectionId: 'conn-1' },
}, send);
expect(sessionBridge.cancelSession).toHaveBeenCalledWith('ws:s1');
expect(sessionBridge.cancel).not.toHaveBeenCalled();
expect((sent[0] as GatewayEvent).event).toBe('done');
});
});
+14
View File
@@ -58,6 +58,20 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
const laneId = sessionId ?? connectionId;
const channel = sessionId?.split(':', 1)[0] ?? 'ws';
const resolvedPolicy = deps.resolveQueuePolicy?.({ laneId, sessionId, connectionId, channel });
const laneQueueWithProcessing = deps.laneQueue as LaneQueue & { isProcessing?: (lane: string) => boolean };
const laneIsProcessing = typeof laneQueueWithProcessing.isProcessing === 'function'
? laneQueueWithProcessing.isProcessing(laneId)
: 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);
}
}
// Enqueue the work — if the lane is idle it runs immediately,
// otherwise it waits for earlier requests on the same session to finish.