mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 16:02:41 +00:00
perf(automations): narrow status observer updates (#17249)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
|
||||
import {
|
||||
selectAutomationAgentStatusEntryChange,
|
||||
UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY
|
||||
} from './automation-agent-status-entry-change'
|
||||
|
||||
function makeEntry(paneKey: string): AgentStatusEntry {
|
||||
return {
|
||||
paneKey,
|
||||
state: 'working',
|
||||
prompt: 'turn',
|
||||
updatedAt: 1,
|
||||
stateStartedAt: 1,
|
||||
stateHistory: []
|
||||
}
|
||||
}
|
||||
|
||||
describe('selectAutomationAgentStatusEntryChange', () => {
|
||||
it('reads only the target and skips an unchanged entry', () => {
|
||||
const targetPaneKey = 'target-tab:leaf'
|
||||
const targetEntry = makeEntry(targetPaneKey)
|
||||
const entries = Object.fromEntries(
|
||||
Array.from({ length: 499 }, (_, index) => {
|
||||
const paneKey = `other-tab:${index}`
|
||||
return [paneKey, makeEntry(paneKey)]
|
||||
})
|
||||
)
|
||||
entries[targetPaneKey] = targetEntry
|
||||
let enumerations = 0
|
||||
const measuredEntries = new Proxy(entries, {
|
||||
ownKeys: (target) => {
|
||||
enumerations += 1
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
})
|
||||
|
||||
expect(selectAutomationAgentStatusEntryChange(measuredEntries, targetPaneKey, undefined)).toBe(
|
||||
targetEntry
|
||||
)
|
||||
expect(
|
||||
selectAutomationAgentStatusEntryChange(measuredEntries, targetPaneKey, targetEntry)
|
||||
).toBe(UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY)
|
||||
expect(enumerations).toBe(0)
|
||||
})
|
||||
|
||||
it('reports removal and ignores inherited or non-enumerable entries', () => {
|
||||
const targetPaneKey = 'target-tab:leaf'
|
||||
const previousEntry = makeEntry(targetPaneKey)
|
||||
const inheritedEntries = Object.create({ [targetPaneKey]: previousEntry }) as Record<
|
||||
string,
|
||||
AgentStatusEntry
|
||||
>
|
||||
const nonEnumerableEntries = Object.defineProperty({}, targetPaneKey, {
|
||||
value: previousEntry
|
||||
}) as Record<string, AgentStatusEntry>
|
||||
|
||||
expect(selectAutomationAgentStatusEntryChange({}, targetPaneKey, previousEntry)).toBeUndefined()
|
||||
expect(selectAutomationAgentStatusEntryChange(inheritedEntries, targetPaneKey, undefined)).toBe(
|
||||
UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY
|
||||
)
|
||||
expect(
|
||||
selectAutomationAgentStatusEntryChange(nonEnumerableEntries, targetPaneKey, undefined)
|
||||
).toBe(UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
|
||||
|
||||
export const UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY = Symbol('unchanged-agent-status-entry')
|
||||
|
||||
export function selectAutomationAgentStatusEntryChange(
|
||||
entries: Readonly<Record<string, AgentStatusEntry>>,
|
||||
targetPaneKey: string,
|
||||
previousEntry: AgentStatusEntry | undefined
|
||||
): AgentStatusEntry | undefined | typeof UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY {
|
||||
const entry = Object.prototype.propertyIsEnumerable.call(entries, targetPaneKey)
|
||||
? entries[targetPaneKey]
|
||||
: undefined
|
||||
// Why: status writes replace entries; unchanged identity proves an unrelated publication.
|
||||
return entry === previousEntry ? UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY : entry
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
|
||||
|
||||
const TARGET_PANE_KEY = 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d'
|
||||
|
||||
export function countUnchangedObserverHistoryReads(
|
||||
state: { agentStatusByPaneKey: Record<string, AgentStatusEntry> },
|
||||
subscriber: (() => void) | null
|
||||
): number {
|
||||
if (!subscriber) {
|
||||
throw new Error('agent status observer was not registered')
|
||||
}
|
||||
let historyReads = 0
|
||||
state.agentStatusByPaneKey = {
|
||||
[TARGET_PANE_KEY]: {
|
||||
paneKey: TARGET_PANE_KEY,
|
||||
state: 'working',
|
||||
prompt: 'turn',
|
||||
updatedAt: Date.now() + 1,
|
||||
stateStartedAt: Date.now() + 1,
|
||||
get stateHistory() {
|
||||
historyReads += 1
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
subscriber()
|
||||
historyReads = 0
|
||||
subscriber()
|
||||
return historyReads
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as ReactModule from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AUTOMATIONS_CHANGED_EVENT } from '@/lib/automations-changed-window-event'
|
||||
import { countUnchangedObserverHistoryReads } from './automation-dispatch-observer-test-probe'
|
||||
|
||||
const mockDispatchEvent = vi.fn()
|
||||
|
||||
@@ -620,10 +621,11 @@ describe('useAutomationDispatchEvents setup launch', () => {
|
||||
await vi.waitFor(() => expect(mockFinalizeTerminalOwnership).toHaveBeenCalledOnce())
|
||||
})
|
||||
|
||||
it('persists assistant output from a batched working→done→working transition', async () => {
|
||||
it('skips unchanged status and persists batched working→done→working output', async () => {
|
||||
const paneKey = 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d'
|
||||
|
||||
await registerAndDispatch()
|
||||
expect(countUnchangedObserverHistoryReads(state, latestStoreSubscriber)).toBe(0)
|
||||
const transitionStartedAt = Date.now() + 1
|
||||
state.agentStatusByPaneKey = {
|
||||
[paneKey]: {
|
||||
|
||||
@@ -32,8 +32,12 @@ import {
|
||||
toSshExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import type { AgentStateHistoryEntry } from '../../../shared/agent-status-types'
|
||||
import type { AgentStateHistoryEntry, AgentStatusEntry } from '../../../shared/agent-status-types'
|
||||
import { resolveFolderWorkspaceHost } from '../../../shared/folder-workspace-execution-host'
|
||||
import {
|
||||
selectAutomationAgentStatusEntryChange,
|
||||
UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY
|
||||
} from './automation-agent-status-entry-change'
|
||||
|
||||
const activeReuseDispatchTabIds = new Set<string>()
|
||||
|
||||
@@ -450,57 +454,64 @@ export function useAutomationDispatchEvents(): void {
|
||||
): void => {
|
||||
let sawWorkingAfterStart = false
|
||||
let observedStateHistory: AgentStateHistoryEntry[] = []
|
||||
let observedEntry: AgentStatusEntry | undefined
|
||||
const checkCurrentStatus = (): void => {
|
||||
const { agentStatusByPaneKey } = useAppStore.getState()
|
||||
for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) {
|
||||
if (paneKey !== targetPaneKey || entry.updatedAt < startedAfter) {
|
||||
const entryChange = selectAutomationAgentStatusEntryChange(
|
||||
useAppStore.getState().agentStatusByPaneKey,
|
||||
targetPaneKey,
|
||||
observedEntry
|
||||
)
|
||||
if (entryChange === UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY) {
|
||||
return
|
||||
}
|
||||
const entry = entryChange
|
||||
observedEntry = entry
|
||||
if (!entry || entry.updatedAt < startedAfter) {
|
||||
return
|
||||
}
|
||||
const historyOverlap = getAgentStateHistoryOverlap(
|
||||
observedStateHistory,
|
||||
entry.stateHistory
|
||||
)
|
||||
// Why: sawWorkingAfterStart stays monotonic — a recreated entry
|
||||
// (transport loss, PTY exit, cap eviction) arrives with an empty
|
||||
// stateHistory, so clearing it here would strand reuseSession runs
|
||||
// with nothing left to re-derive the working edge from.
|
||||
for (const historicalState of entry.stateHistory.slice(historyOverlap)) {
|
||||
if (historicalState.startedAt < startedAfter) {
|
||||
continue
|
||||
}
|
||||
const historyOverlap = getAgentStateHistoryOverlap(
|
||||
observedStateHistory,
|
||||
entry.stateHistory
|
||||
)
|
||||
// Why: sawWorkingAfterStart stays monotonic — a recreated entry
|
||||
// (transport loss, PTY exit, cap eviction) arrives with an empty
|
||||
// stateHistory, so clearing it here would strand reuseSession runs
|
||||
// with nothing left to re-derive the working edge from.
|
||||
for (const historicalState of entry.stateHistory.slice(historyOverlap)) {
|
||||
if (historicalState.startedAt < startedAfter) {
|
||||
continue
|
||||
}
|
||||
if (historicalState.state === 'working') {
|
||||
sawWorkingAfterStart = true
|
||||
}
|
||||
if (
|
||||
historicalState.state === 'done' &&
|
||||
(!options?.requireWorkingAfterStart || sawWorkingAfterStart)
|
||||
) {
|
||||
// Why: this `done` already rolled out of the live entry, so its output
|
||||
// survives only in the entry-level completed slot.
|
||||
latestAssistantMessage =
|
||||
entry.lastCompletedAssistantMessage?.trim() || latestAssistantMessage
|
||||
handleAgentDone()
|
||||
return
|
||||
}
|
||||
}
|
||||
observedStateHistory = [...entry.stateHistory]
|
||||
if (entry.state === 'working') {
|
||||
if (historicalState.state === 'working') {
|
||||
sawWorkingAfterStart = true
|
||||
}
|
||||
if (
|
||||
entry.state === 'done' &&
|
||||
// Why: a session-boundary done is the agent CONNECTING (Claude SessionStart
|
||||
// fires at launch, before the argv prompt submits) — completing here would
|
||||
// close the tab and record an empty run result.
|
||||
entry.sessionBoundary !== true &&
|
||||
historicalState.state === 'done' &&
|
||||
(!options?.requireWorkingAfterStart || sawWorkingAfterStart)
|
||||
) {
|
||||
// Why: this `done` already rolled out of the live entry, so its output
|
||||
// survives only in the entry-level completed slot.
|
||||
latestAssistantMessage =
|
||||
entry.lastAssistantMessage?.trim() || latestAssistantMessage
|
||||
entry.lastCompletedAssistantMessage?.trim() || latestAssistantMessage
|
||||
handleAgentDone()
|
||||
return
|
||||
}
|
||||
}
|
||||
observedStateHistory = [...entry.stateHistory]
|
||||
if (entry.state === 'working') {
|
||||
sawWorkingAfterStart = true
|
||||
}
|
||||
if (
|
||||
entry.state === 'done' &&
|
||||
// Why: a session-boundary done is the agent CONNECTING (Claude SessionStart
|
||||
// fires at launch, before the argv prompt submits) — completing here would
|
||||
// close the tab and record an empty run result.
|
||||
entry.sessionBoundary !== true &&
|
||||
(!options?.requireWorkingAfterStart || sawWorkingAfterStart)
|
||||
) {
|
||||
latestAssistantMessage =
|
||||
entry.lastAssistantMessage?.trim() || latestAssistantMessage
|
||||
handleAgentDone()
|
||||
}
|
||||
}
|
||||
// Why: Codex/Claude completion normally arrives through the global
|
||||
// hook IPC listener, not the hidden PTY OSC fallback.
|
||||
|
||||
Reference in New Issue
Block a user