mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(notifications): retain paired client turn boundary
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
createAgentCompletionCoordinator,
|
||||
resetAgentCompletionCoordinatorIdentitiesForTest
|
||||
} from '@/components/terminal-pane/agent-completion-coordinator'
|
||||
import { dispatchTerminalNotification } from '@/components/terminal-pane/use-notification-dispatch'
|
||||
import {
|
||||
markRendererOwnedAgentStatusWrite,
|
||||
registerRendererOwnedAgentStatusPane,
|
||||
@@ -97,6 +98,7 @@ describe('paired session-tab agent completion notifications', () => {
|
||||
useAppStore.setState(initialState, true)
|
||||
resetWebSessionTabsSnapshotFreshnessForTests()
|
||||
resetRendererOwnedAgentStatusPanesForTests()
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
@@ -206,6 +208,55 @@ describe('paired session-tab agent completion notifications', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a delayed host stamp bound to the prior client turn', () => {
|
||||
const paneKey = makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID)
|
||||
registerRendererOwnedAgentStatusPane(paneKey, ENVIRONMENT_ID)
|
||||
markRendererOwnedAgentStatusWrite(paneKey)
|
||||
const setClientTurn = (stateStartedAt: number, prompt: string): void => {
|
||||
useAppStore.setState({
|
||||
agentStatusByPaneKey: {
|
||||
[paneKey]: {
|
||||
state: 'working',
|
||||
prompt,
|
||||
updatedAt: NOW,
|
||||
stateStartedAt,
|
||||
agentType: 'claude',
|
||||
paneKey,
|
||||
tabId: toWebTerminalSurfaceTabId(HOST_TAB_ID),
|
||||
worktreeId: WORKTREE_ID,
|
||||
stateHistory: []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setClientTurn(5_000, 'first turn')
|
||||
applySnapshot(makeAgentSnapshot(1, NOW - 2_000), true)
|
||||
setClientTurn(6_000, 'second turn')
|
||||
applySnapshot(makeAgentSnapshot(2, NOW - 1_500), true)
|
||||
const turnCompletedAt = NOW + 20_000
|
||||
applySnapshot(makeAgentSnapshot(3, NOW - 1_000, turnCompletedAt), true)
|
||||
|
||||
const delayedPayload =
|
||||
mocks.observeAgentHookCompletionForNotification.mock.calls.at(-1)?.[0]?.payload
|
||||
expect(delayedPayload).toMatchObject({
|
||||
localStateStartedAt: 5_000,
|
||||
turnCompletedAt
|
||||
})
|
||||
|
||||
const notificationDispatch = vi.fn()
|
||||
vi.stubGlobal('window', { api: { notifications: { dispatch: notificationDispatch } } })
|
||||
dispatchTerminalNotification(WORKTREE_ID, {
|
||||
source: 'agent-task-complete',
|
||||
terminalTitle: 'Claude working',
|
||||
paneKey,
|
||||
agentCompletionSource: 'hook',
|
||||
agentStatusSnapshot: delayedPayload
|
||||
})
|
||||
|
||||
expect(notificationDispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('suppresses a stamped reconnect tail and its live all-clear', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
|
||||
@@ -157,6 +157,17 @@ const hostSessionTabMappingKeysByEnvironmentAndWorktree = new Map<
|
||||
string,
|
||||
Map<string, Set<string>>
|
||||
>()
|
||||
// Why: a delayed host stamp belongs to the client boundary seen with its preceding ordered frame.
|
||||
const hostWorkingClientBoundaryByPaneKey = new Map<
|
||||
string,
|
||||
{
|
||||
hostStateStartedAt: number
|
||||
hostPrompt: string
|
||||
clientStateStartedAt: number
|
||||
stamped: boolean
|
||||
}
|
||||
>()
|
||||
const HOST_WORKING_CLIENT_BOUNDARY_LIMIT = 512
|
||||
let receivedSessionTabsFrameSequence = 0
|
||||
|
||||
type TerminalSurface = RuntimeMobileSessionTerminalClientTab
|
||||
@@ -643,6 +654,7 @@ export function resetWebSessionTabsSnapshotFreshnessForTests(): void {
|
||||
lastHostTerminalTabCountByWorktree.clear()
|
||||
hostSessionTabIdByLocalKey.clear()
|
||||
hostSessionTabMappingKeysByEnvironmentAndWorktree.clear()
|
||||
hostWorkingClientBoundaryByPaneKey.clear()
|
||||
resetWebSessionBrowserPlacementsForTests()
|
||||
}
|
||||
|
||||
@@ -3450,6 +3462,9 @@ export function applyWebSessionTabsStorePatch(
|
||||
const turnCompletedAt = remapped
|
||||
? normalizeTurnCompletedAtField(surface.turnCompletedAt, remapped.state)
|
||||
: undefined
|
||||
if (remapped?.state === 'done' && turnCompletedAt === undefined) {
|
||||
hostWorkingClientBoundaryByPaneKey.delete(remapped.paneKey)
|
||||
}
|
||||
// Why: client OSC owns display state, but only the host hook stream carries background-turn stamps.
|
||||
const clientOwnedNotification = Boolean(
|
||||
remapped &&
|
||||
@@ -3469,6 +3484,52 @@ export function applyWebSessionTabsStorePatch(
|
||||
if (!notificationStatus) {
|
||||
continue
|
||||
}
|
||||
const currentClientStateStartedAt = clientOwnedNotification
|
||||
? state.agentStatusByPaneKey[notificationStatus.paneKey]?.stateStartedAt
|
||||
: undefined
|
||||
let localStateStartedAt = currentClientStateStartedAt
|
||||
if (
|
||||
clientOwnedNotification &&
|
||||
notificationStatus.state === 'working' &&
|
||||
currentClientStateStartedAt !== undefined
|
||||
) {
|
||||
if (turnCompletedAt === undefined) {
|
||||
const retainedBoundary = hostWorkingClientBoundaryByPaneKey.get(
|
||||
notificationStatus.paneKey
|
||||
)
|
||||
if (
|
||||
!retainedBoundary ||
|
||||
retainedBoundary.hostStateStartedAt !== notificationStatus.stateStartedAt ||
|
||||
retainedBoundary.hostPrompt !== notificationStatus.prompt ||
|
||||
retainedBoundary.stamped
|
||||
) {
|
||||
hostWorkingClientBoundaryByPaneKey.delete(notificationStatus.paneKey)
|
||||
hostWorkingClientBoundaryByPaneKey.set(notificationStatus.paneKey, {
|
||||
hostStateStartedAt: notificationStatus.stateStartedAt,
|
||||
hostPrompt: notificationStatus.prompt,
|
||||
clientStateStartedAt: currentClientStateStartedAt,
|
||||
stamped: false
|
||||
})
|
||||
if (hostWorkingClientBoundaryByPaneKey.size > HOST_WORKING_CLIENT_BOUNDARY_LIMIT) {
|
||||
const oldestPaneKey = hostWorkingClientBoundaryByPaneKey.keys().next().value
|
||||
if (oldestPaneKey !== undefined) {
|
||||
hostWorkingClientBoundaryByPaneKey.delete(oldestPaneKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const retainedBoundary = hostWorkingClientBoundaryByPaneKey.get(
|
||||
notificationStatus.paneKey
|
||||
)
|
||||
if (
|
||||
retainedBoundary?.hostStateStartedAt === notificationStatus.stateStartedAt &&
|
||||
retainedBoundary.hostPrompt === notificationStatus.prompt
|
||||
) {
|
||||
localStateStartedAt = retainedBoundary.clientStateStartedAt
|
||||
retainedBoundary.stamped = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allowCompletionNotification && notificationStatus.state !== 'working') {
|
||||
continue
|
||||
}
|
||||
@@ -3484,8 +3545,7 @@ export function applyWebSessionTabsStorePatch(
|
||||
stateStartedAt: notificationStatus.stateStartedAt,
|
||||
...(clientOwnedNotification
|
||||
? {
|
||||
localStateStartedAt:
|
||||
state.agentStatusByPaneKey[notificationStatus.paneKey]?.stateStartedAt
|
||||
localStateStartedAt
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user