diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts new file mode 100644 index 00000000000..9d83b1d6a93 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest' +import { createAgentCompletionCoordinator } from './agent-completion-coordinator' +import { + createDeferred, + flushAsyncTicks, + processResult, + useAgentCompletionCoordinatorLifecycle +} from './agent-completion-coordinator-test-harness' +import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection' + +describe('agent completion coordinator queued inspections', () => { + useAgentCompletionCoordinatorLifecycle() + + it('drops inspections queued by a disposed coordinator before starting live work', async () => { + const blockers = Array.from({ length: 4 }, () => + createDeferred() + ) + const blockerInspectors = blockers.map((inspection) => vi.fn(() => inspection.promise)) + const blockerCoordinators = blockerInspectors.map((inspectProcess, index) => + createAgentCompletionCoordinator({ + paneKey: `tab-1:blocked-${index}`, + getPtyId: () => `pty-blocked-${index}`, + getSettings: () => null, + inspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + ) + + blockerCoordinators.forEach((coordinator) => coordinator.startProcessTracking()) + await vi.advanceTimersByTimeAsync(2_000) + expect( + blockerInspectors.every((inspectProcess) => inspectProcess.mock.calls.length === 1) + ).toBe(true) + + const staleInspectProcesses = Array.from({ length: 8 }, () => + vi.fn(async () => processResult(null, false)) + ) + const staleCoordinators = staleInspectProcesses.map((inspectProcess, index) => + createAgentCompletionCoordinator({ + paneKey: `tab-1:stale-${index}`, + getPtyId: () => `pty-stale-${index}`, + getSettings: () => null, + inspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + ) + const liveInspectProcess = vi.fn(async () => processResult(null, false)) + const liveCoordinator = createAgentCompletionCoordinator({ + paneKey: 'tab-1:live', + getPtyId: () => 'pty-live', + getSettings: () => null, + inspectProcess: liveInspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + + for (const [index, coordinator] of staleCoordinators.entries()) { + coordinator.observeTitle(`Codex working ${index}`) + coordinator.observeTitle(`~/stale-${index}`) + } + liveCoordinator.observeTitle('Codex working') + liveCoordinator.observeTitle('~/live') + staleCoordinators.forEach((coordinator) => coordinator.dispose()) + + blockers.forEach((inspection) => inspection.resolve(processResult(null, false))) + await flushAsyncTicks() + // Existing 100ms pump admits live work after blockers release. + await vi.advanceTimersByTimeAsync(100) + await flushAsyncTicks() + + expect( + staleInspectProcesses.every((inspectProcess) => inspectProcess.mock.calls.length === 0) + ).toBe(true) + expect(liveInspectProcess).toHaveBeenCalledTimes(1) + + blockerCoordinators.forEach((coordinator) => coordinator.dispose()) + liveCoordinator.dispose() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts index 39ca68e1ad4..f0a0492fdfe 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts @@ -146,6 +146,7 @@ export function createAgentCompletionProcessMonitor({ const pendingTitleIdAtRequest = priority === 'pending-title' ? pendingTitle.get()?.id : null enqueueAgentProcessInspection({ priority, + canRun: () => !state.disposed, run: async () => { let inspectedRecognizedAgent = false let inspectionSucceeded = false diff --git a/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts b/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts index 5c581ddc652..f7512ad8c60 100644 --- a/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts +++ b/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts @@ -2,6 +2,7 @@ export type InspectionPriority = 'cadence' | 'pending-title' type InspectionTask = { priority: InspectionPriority + canRun: () => boolean run: () => Promise } @@ -37,6 +38,16 @@ function scheduleInspectionPump(delayMs = 0): void { } function pumpInspectionQueue(): void { + // Drop disposed tasks before slot/rate accounting. + for (let index = inspectionQueue.length - 1; index >= 0; index -= 1) { + const task = inspectionQueue[index] + if (task && !task.canRun()) { + inspectionQueue.splice(index, 1) + } + } + if (inspectionQueue.length === 0) { + return + } const now = Date.now() if (!canStartInspection(now)) { scheduleInspectionPump(100)