Seed startup replay and release stale fallback dedupe

This commit is contained in:
Brennan Benson
2026-08-14 17:04:03 -07:00
parent 8b22d411a8
commit 1056bc4452
5 changed files with 102 additions and 6 deletions
@@ -1574,6 +1574,70 @@ describe('agent completion coordinator', () => {
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
})
it.each(['pty-first', 'host-first'] as const)(
'releases stamped fallback dedupe when the next turn arrives %s',
(workingOrder) => {
const dispatchCompletion = vi.fn()
const localCoordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
statusLane: 'pty',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
const hostCoordinator = createAgentCompletionCoordinator({
paneKey: 'tab-1:leaf-1',
statusLane: 'hook',
getPtyId: () => 'pty-1',
getSettings: () => null,
inspectProcess: vi.fn(),
dispatchCompletion,
isLive: () => true
})
const observeHostNextTurn = () =>
hostCoordinator.observeHookStatus({
state: 'working',
prompt: 'second turn',
agentType: 'claude',
stateStartedAt: 2_000
})
localCoordinator.observeHookStatus({
state: 'working',
prompt: 'first turn',
agentType: 'claude',
stateStartedAt: 5_000
})
hostCoordinator.observeHookStatus({
state: 'working',
prompt: 'first turn',
agentType: 'claude',
stateStartedAt: 2_000
})
hostCoordinator.observeHookStatus({
state: 'working',
prompt: 'first turn',
agentType: 'claude',
stateStartedAt: 2_000,
turnCompletedAt: 2_500
})
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
if (workingOrder === 'pty-first') {
localCoordinator.observeTitleWorking()
observeHostNextTurn()
} else {
observeHostNextTurn()
localCoordinator.observeTitleWorking()
}
localCoordinator.observeClassifiedTitleCompletion('Claude done')
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
}
)
it.each(['before', 'after'] as const)(
'keeps sibling tail replay suppression when the pane remounts %s its all-clear',
(remountOrder) => {
@@ -1035,6 +1035,12 @@ export function createAgentCompletionCoordinator(
const pendingStampedTail = pendingStampedTailByPaneKey.get(options.paneKey)
if (pendingStampedTail?.originLane === coordinatorLane) {
pendingStampedTailByPaneKey.delete(options.paneKey)
if (
lastCompletionIdentityByPaneKey.get(options.paneKey)?.lastTurnCompletedAtNotified ===
pendingStampedTail.turnCompletedAt
) {
lastCompletionIdentityByPaneKey.delete(options.paneKey)
}
}
recordWorkingBoundary(payload.stateStartedAt)
clearPendingHookDone()
@@ -281,12 +281,18 @@ export function observeAgentHookCompletionForNotification({
payload: AgentCompletionStatusSnapshot
seedOnly?: boolean
}): void {
pruneClosedPaneCoordinators()
if (!paneCanReceiveHookCompletion(paneKey)) {
return
// Why: replay seeds already passed indexed snapshot ownership; re-resolving every row makes startup batches quadratic.
if (seedOnly !== true) {
pruneClosedPaneCoordinators()
if (!paneCanReceiveHookCompletion(paneKey)) {
return
}
}
const trackingEnabled = syncAgentHookCompletionNotificationSettings()
const trackingEnabled =
seedOnly === true
? isAgentTaskCompleteTrackingEnabled()
: syncAgentHookCompletionNotificationSettings()
let entry = coordinatorsByPaneKey.get(paneKey)
if (!entry || entry.worktreeId !== worktreeId) {
@@ -9160,6 +9160,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
it('queues replayed startup snapshots until the pane hydrates', async () => {
const setAgentStatus = vi.fn()
const observeAgentHookCompletionForNotification = vi.fn()
const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null }
let resolveSnapshot!: (entries: AgentStatusSetData[]) => void
const getSnapshot = vi.fn(
@@ -9190,6 +9191,12 @@ describe('useIpcEvents agent status snapshot integration', () => {
getState: () => storeState
}
}))
vi.doMock('./agent-hook-completion-notifications', () => ({
observeAgentHookCompletionForNotification,
resetAgentHookCompletionNotificationCoordinators: vi.fn(),
syncAgentHookCompletionNotificationSettings: vi.fn(),
syncAgentHookCompletionNotificationsForStoreUpdate: vi.fn()
}))
stubAuxiliaryModules()
vi.stubGlobal(
'window',
@@ -9215,6 +9222,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
agentType: 'codex',
toolName: 'Bash',
toolInput: 'pnpm test',
turnCompletedAt: 1_699_999_999_500,
terminalHandle: 'term-future',
receivedAt: 1_700_000_000_000,
stateStartedAt: 1_699_999_999_000
@@ -9254,6 +9262,17 @@ describe('useIpcEvents agent status snapshot integration', () => {
expectWorktreeRouting('wt-1'),
undefined
)
expect(observeAgentHookCompletionForNotification).toHaveBeenCalledWith(
expect.objectContaining({
paneKey: FUTURE_PANE_KEY,
worktreeId: 'wt-1',
seedOnly: true,
payload: expect.objectContaining({
state: 'working',
turnCompletedAt: 1_699_999_999_500
})
})
)
})
it('suppresses notifications for replayed done snapshots after pane hydration', async () => {
+3 -2
View File
@@ -3386,7 +3386,7 @@ export function useIpcEvents(): void {
: undefined
}
const applyPostCommitNotification = (): void => {
if (options?.replay !== true && statusWorktreeId) {
if (statusWorktreeId && (options?.replay !== true || resolvedPayload.state === 'working')) {
// Why: local Codex/Claude hooks arrive via this main-process IPC path, not the PTY OSC fallback, so task-complete notifications must observe accepted hook state here too.
const notificationPayload =
typeof data.stateStartedAt === 'number'
@@ -3395,7 +3395,8 @@ export function useIpcEvents(): void {
observeAgentHookCompletionForNotification({
paneKey,
worktreeId: statusWorktreeId,
payload: notificationPayload
payload: notificationPayload,
...(options?.replay === true ? { seedOnly: true } : {})
})
}
}