Deduplicate paired completion tails

This commit is contained in:
Brennan Benson
2026-08-14 15:52:50 -07:00
parent fdeef5b662
commit ca2e71c0ae
7 changed files with 180 additions and 7 deletions
@@ -5,6 +5,8 @@ import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-termina
export type AgentCompletionStatusSnapshot = ParsedAgentStatusPayload & {
stateStartedAt?: number
/** Renderer-local boundary used only to reject a delayed cross-host completion. */
localStateStartedAt?: number
}
export type AgentCompletionDispatchMeta = {
@@ -1328,6 +1328,70 @@ describe('agent completion coordinator', () => {
)
})
it.each(['before', 'after'] as const)(
'does not let a sibling OSC all-clear arriving %s the host stamp duplicate it',
(allClearOrder) => {
const dispatchCompletion = vi.fn()
const localCoordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
const hostCoordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
localCoordinator.observeHookStatus({
state: 'working',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: 5_000
})
hostCoordinator.observeHookStatus({
state: 'working',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: 2_000
})
if (allClearOrder === 'before') {
localCoordinator.observeHookStatus({
state: 'done',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: 5_500
})
}
hostCoordinator.observeHookStatus({
state: 'working',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: 2_000,
turnCompletedAt: 2_500
})
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
if (allClearOrder === 'after') {
localCoordinator.observeHookStatus({
state: 'done',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: 5_500
})
}
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
}
)
it('does not replay a stamped turn or its all-clear after a working title blip', () => {
const dispatchCompletion = vi.fn()
const dispatchHookLifecycle = vi.fn()
@@ -211,6 +211,21 @@ export function createAgentCompletionCoordinator(
)
}
function consumePendingStampedTailForAgent(agentIdentity: string | null): boolean {
const pendingStampedTail = pendingStampedTailByPaneKey.get(options.paneKey)
const stampedCompletion = lastCompletionIdentityByPaneKey.get(options.paneKey)
if (
pendingStampedTail === undefined ||
stampedCompletion?.lastTurnCompletedAtNotified !== pendingStampedTail ||
agentIdentity === null ||
stampedCompletion.agentIdentity !== agentIdentity
) {
return false
}
pendingStampedTailByPaneKey.delete(options.paneKey)
return true
}
function hookCompletionAgentIdentity(payload: AgentCompletionStatusSnapshot): string | null {
return payload.agentType?.trim().toLowerCase() || null
}
@@ -283,7 +298,11 @@ export function createAgentCompletionCoordinator(
return false
}
if (previous.source === completionIdentity.source) {
return previous.identity === completionIdentity.identity
return (
previous.identity === completionIdentity.identity ||
(completionIdentity.source === 'hook' &&
consumePendingStampedTailForAgent(completionIdentity.agentIdentity))
)
}
return (
previous.agentIdentity !== null &&
@@ -943,6 +962,14 @@ export function createAgentCompletionCoordinator(
const turnCompletedAt = isFiniteTurnCompletedAt(payload.turnCompletedAt)
? payload.turnCompletedAt
: undefined
if (
turnCompletedAt === undefined &&
consumePendingStampedTailForAgent(hookCompletionAgentIdentity(payload))
) {
// Why: paired host hooks carry the stamp while the sibling OSC coordinator sees only the matching all-clear.
options.dispatchHookLifecycle?.(payload)
return
}
if (
turnCompletedAt !== undefined &&
(turnCompletedAtAlreadyHandled(turnCompletedAt) ||
@@ -8,16 +8,20 @@ export function isSupersededAgentCompletionSnapshot(
if (!storedAgentStatus || !snapshot) {
return false
}
if (typeof snapshot.stateStartedAt !== 'number') {
const comparableStateStartedAt = snapshot.localStateStartedAt ?? snapshot.stateStartedAt
if (typeof comparableStateStartedAt !== 'number') {
return storedAgentStatus.state !== snapshot.state
}
// Why: hook completion notifications are delayed by a quiet window; by the
// time they fire, the same pane may already belong to a newer agent turn.
if (storedAgentStatus.stateStartedAt > snapshot.stateStartedAt) {
if (storedAgentStatus.stateStartedAt > comparableStateStartedAt) {
return true
}
const hasStampedTurn =
typeof snapshot.turnCompletedAt === 'number' && Number.isFinite(snapshot.turnCompletedAt)
return (
storedAgentStatus.stateStartedAt === snapshot.stateStartedAt &&
storedAgentStatus.state !== snapshot.state
storedAgentStatus.stateStartedAt === comparableStateStartedAt &&
storedAgentStatus.state !== snapshot.state &&
!hasStampedTurn
)
}
@@ -225,6 +225,64 @@ describe('dispatchTerminalNotification', () => {
)
})
it.each([
{ clientStateStartedAt: 5_000, hostTurnCompletedAt: 2_000 },
{ clientStateStartedAt: 2_000, hostTurnCompletedAt: 5_000 }
])(
'accepts a host-stamped completion across client/host clock skew %#',
({ clientStateStartedAt, hostTurnCompletedAt }) => {
mockState.agentStatusByPaneKey[paneKey] = makeAgentStatus(paneKey, {
state: 'working',
stateStartedAt: clientStateStartedAt,
agentType: 'claude',
terminalTitle: 'claude'
})
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'claude',
paneKey,
agentStatusSnapshot: {
state: 'done',
prompt: 'review the PR',
agentType: 'claude',
stateStartedAt: hostTurnCompletedAt,
localStateStartedAt: clientStateStartedAt,
turnCompletedAt: hostTurnCompletedAt
}
})
expect(window.api.notifications.dispatch).toHaveBeenCalledWith(
expect.objectContaining({ source: 'agent-task-complete' })
)
}
)
it('drops a host-stamped completion after a newer client turn starts', () => {
mockState.agentStatusByPaneKey[paneKey] = makeAgentStatus(paneKey, {
state: 'working',
stateStartedAt: 6_000,
agentType: 'claude',
terminalTitle: 'claude'
})
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'claude',
paneKey,
agentStatusSnapshot: {
state: 'done',
prompt: 'previous turn',
agentType: 'claude',
stateStartedAt: 2_000,
localStateStartedAt: 5_000,
turnCompletedAt: 2_000
}
})
expect(window.api.notifications.dispatch).not.toHaveBeenCalled()
})
it('uses a live pane key when inactive worktree tab membership is not hydrated', () => {
mockState.tabsByWorktree = {}
@@ -177,6 +177,15 @@ describe('paired session-tab agent completion notifications', () => {
expect(useAppStore.getState().agentStatusByPaneKey[paneKey]?.state).toBe('working')
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(3)
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
payload: expect.objectContaining({
localStateStartedAt: NOW,
turnCompletedAt
})
})
)
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
expect(dispatchCompletion).toHaveBeenCalledWith(
'claude',
@@ -3426,7 +3426,10 @@ export function applyWebSessionTabsStorePatch(
const acceptedNotificationStatuses: {
paneKey: string
worktreeId: string
payload: ReturnType<typeof pickParsedAgentStatusPayload> & { stateStartedAt: number }
payload: ReturnType<typeof pickParsedAgentStatusPayload> & {
stateStartedAt: number
localStateStartedAt?: number
}
}[] = []
useAppStore.setState((state) => {
const patch = buildPatch(state)
@@ -3476,7 +3479,13 @@ export function applyWebSessionTabsStorePatch(
...notificationStatus,
...(turnCompletedAt !== undefined ? { turnCompletedAt } : {})
}),
stateStartedAt: notificationStatus.stateStartedAt
stateStartedAt: notificationStatus.stateStartedAt,
...(clientOwnedNotification
? {
localStateStartedAt:
state.agentStatusByPaneKey[notificationStatus.paneKey]?.stateStartedAt
}
: {})
}
})
}