mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
perf(sleep): scope hibernation rechecks to pane owner
This commit is contained in:
@@ -543,6 +543,53 @@ describe('agent sleep coordinator', () => {
|
||||
).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('revalidates a confirmed pane without listing unrelated runtime worktrees', async () => {
|
||||
installRuntimeListResponses(...Array.from({ length: 5 }, () => runtimeListResult(['pty-1'])))
|
||||
const shutdown = installEligibleState(vi.fn().mockResolvedValue(undefined), {
|
||||
settings: {
|
||||
experimentalAgentHibernation: true,
|
||||
agentHibernationIdleMs: DEFAULT_AGENT_HIBERNATION_IDLE_MS,
|
||||
activeRuntimeEnvironmentId: 'runtime-1'
|
||||
} as never,
|
||||
worktreesByRepo: {
|
||||
'fixture-repo': [
|
||||
{
|
||||
id: 'wt-bg',
|
||||
repoId: 'fixture-repo',
|
||||
hostId: 'runtime:runtime-1',
|
||||
runtimeOwnerEnvironmentId: 'runtime-1'
|
||||
},
|
||||
{
|
||||
id: 'wt-unrelated',
|
||||
repoId: 'fixture-repo',
|
||||
hostId: 'runtime:runtime-2',
|
||||
runtimeOwnerEnvironmentId: 'runtime-2'
|
||||
}
|
||||
]
|
||||
} as never,
|
||||
tabsByWorktree: {
|
||||
'wt-bg': [tab()],
|
||||
'wt-unrelated': [{ ...tab(), id: 'tab-2', worktreeId: 'wt-unrelated' }]
|
||||
},
|
||||
ptyIdsByTabId: { 'tab-1': [] }
|
||||
})
|
||||
|
||||
await runAgentHibernationTick()
|
||||
await runAgentHibernationTick()
|
||||
|
||||
expect(shutdown).toHaveBeenCalledTimes(1)
|
||||
const listCalls = mockRuntimeEnvironmentCall.mock.calls.filter(
|
||||
([args]) => args.method === 'terminal.list'
|
||||
)
|
||||
// Two global confirmation samples list both worktrees; the destructive
|
||||
// recheck lists only the candidate's owner: 2W + C, not 2W + C×W.
|
||||
expect(listCalls).toHaveLength(5)
|
||||
expect(listCalls.at(-1)?.[0]).toMatchObject({
|
||||
selector: 'runtime-1',
|
||||
params: { worktree: expect.anything() }
|
||||
})
|
||||
})
|
||||
|
||||
it('uses fresh store state after awaiting runtime liveness before shutdown', async () => {
|
||||
vi.useFakeTimers()
|
||||
const delayed = deferred<ReturnType<typeof runtimeListResult>>()
|
||||
|
||||
@@ -63,13 +63,16 @@ type RuntimePtyLivenessSample = {
|
||||
function snapshotFromState(
|
||||
state: AppState,
|
||||
now: number,
|
||||
runtimeLiveness: RuntimePtyLivenessSample
|
||||
runtimeLiveness: RuntimePtyLivenessSample,
|
||||
targetWorktreeId?: string
|
||||
): AgentHibernationPlannerSnapshot {
|
||||
return {
|
||||
settings: state.settings,
|
||||
activeWorktreeId: state.activeWorktreeId,
|
||||
foregroundTerminalTabIds: getForegroundTerminalTabIds(),
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
tabsByWorktree: targetWorktreeId
|
||||
? { [targetWorktreeId]: state.tabsByWorktree[targetWorktreeId] ?? [] }
|
||||
: state.tabsByWorktree,
|
||||
terminalLayoutsByTabId: state.terminalLayoutsByTabId,
|
||||
ptyIdsByTabId: state.ptyIdsByTabId,
|
||||
runtimeLivePtyIdsByWorktreeId: runtimeLiveness.runtimeLivePtyIdsByWorktreeId,
|
||||
@@ -90,9 +93,17 @@ function snapshotFromState(
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeLivenessTargetWorktrees(state: AppState): Map<string, string> {
|
||||
function getRuntimeLivenessTargetWorktrees(
|
||||
state: AppState,
|
||||
targetWorktreeId?: string
|
||||
): Map<string, string> {
|
||||
const targets = new Map<string, string>()
|
||||
for (const worktreeId of Object.keys(state.tabsByWorktree)) {
|
||||
const worktreeIds = targetWorktreeId
|
||||
? Object.hasOwn(state.tabsByWorktree, targetWorktreeId)
|
||||
? [targetWorktreeId]
|
||||
: []
|
||||
: Object.keys(state.tabsByWorktree)
|
||||
for (const worktreeId of worktreeIds) {
|
||||
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, worktreeId)
|
||||
if (runtimeEnvironmentId) {
|
||||
targets.set(worktreeId, runtimeEnvironmentId)
|
||||
@@ -111,8 +122,11 @@ function getTypedRuntimePtyId(terminal: RuntimeTerminalSummary): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
async function collectRuntimePtyLiveness(state: AppState): Promise<RuntimePtyLivenessSample> {
|
||||
const targets = getRuntimeLivenessTargetWorktrees(state)
|
||||
async function collectRuntimePtyLiveness(
|
||||
state: AppState,
|
||||
targetWorktreeId?: string
|
||||
): Promise<RuntimePtyLivenessSample> {
|
||||
const targets = getRuntimeLivenessTargetWorktrees(state, targetWorktreeId)
|
||||
const runtimeLivePtyIdsByWorktreeId: Record<string, string[]> = {}
|
||||
const runtimeLivenessRequiredWorktreeIds = [...targets.keys()]
|
||||
await Promise.all(
|
||||
@@ -152,8 +166,8 @@ async function collectRuntimePtyLiveness(state: AppState): Promise<RuntimePtyLiv
|
||||
return { runtimeLivePtyIdsByWorktreeId, runtimeLivenessRequiredWorktreeIds }
|
||||
}
|
||||
|
||||
async function currentCandidates(now: number) {
|
||||
const runtimeLiveness = await collectRuntimePtyLiveness(useAppStore.getState())
|
||||
async function currentCandidates(now: number, targetWorktreeId?: string) {
|
||||
const runtimeLiveness = await collectRuntimePtyLiveness(useAppStore.getState(), targetWorktreeId)
|
||||
const freshState = useAppStore.getState()
|
||||
// Why: age the PTY bindings from the same state the plan is built from, so a pane
|
||||
// observed for the first time this pass cannot also be judged long-idle in it.
|
||||
@@ -163,7 +177,9 @@ async function currentCandidates(now: number) {
|
||||
now,
|
||||
idleMs: getEffectiveAgentHibernationIdleMs(freshState.settings?.agentHibernationIdleMs)
|
||||
})
|
||||
return planAgentHibernationCandidates(snapshotFromState(freshState, now, runtimeLiveness))
|
||||
return planAgentHibernationCandidates(
|
||||
snapshotFromState(freshState, now, runtimeLiveness, targetWorktreeId)
|
||||
)
|
||||
.filter((candidate) => {
|
||||
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(
|
||||
freshState,
|
||||
@@ -186,7 +202,9 @@ async function hibernatePaneIfStillEligible(
|
||||
if (coordinator.shuttingDownCandidateIds.has(id)) {
|
||||
return
|
||||
}
|
||||
const candidates = await currentCandidates(coordinator.now())
|
||||
// Why: the confirmed pane can only be authorized by its owning worktree. A
|
||||
// global sweep here made C pane teardowns issue C×W fresh runtime listings.
|
||||
const candidates = await currentCandidates(coordinator.now(), worktreeId)
|
||||
const stillEligible = candidates.some(
|
||||
(candidate) =>
|
||||
candidate.id === confirmedCandidate.id && candidate.signature === confirmedCandidate.signature
|
||||
|
||||
@@ -65,16 +65,21 @@ describe('hibernation PTY binding age', () => {
|
||||
// The planner already fails closed during the gap; dropping the binding here would
|
||||
// hand the same PTY a fresh idle window every time the layout flickers.
|
||||
observe('pty-1', 1_000)
|
||||
recordHibernationBoundaryResolved(PANE, 1_500)
|
||||
observe(null, 2_000)
|
||||
expect(getHibernationPtyBindingFirstSeenAtByPaneKey()[PANE]).toBe(1_000)
|
||||
expect(getHibernationBoundaryResolvedAtByPaneKey()[PANE]).toBe(1_500)
|
||||
observe('pty-1', 3_000)
|
||||
expect(getHibernationPtyBindingFirstSeenAtByPaneKey()[PANE]).toBe(1_000)
|
||||
expect(getHibernationBoundaryResolvedAtByPaneKey()[PANE]).toBe(1_500)
|
||||
})
|
||||
|
||||
it('expires an unseen binding once it is older than the idle window', () => {
|
||||
observe('pty-1', 1_000)
|
||||
recordHibernationBoundaryResolved(PANE, 1_000)
|
||||
observe(null, 1_000 + IDLE_MS + 1)
|
||||
expect(getHibernationPtyBindingFirstSeenAtByPaneKey()[PANE]).toBeUndefined()
|
||||
expect(getHibernationBoundaryResolvedAtByPaneKey()[PANE]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops bindings and boundary stamps when the tab is authoritatively gone', () => {
|
||||
|
||||
@@ -63,7 +63,10 @@ export function observeHibernationPtyBindings(args: {
|
||||
}
|
||||
for (const paneKey of boundaryResolvedAtByPaneKey.keys()) {
|
||||
const tabId = paneKey.slice(0, paneKey.indexOf(':'))
|
||||
if (!liveTabIds.has(tabId)) {
|
||||
// Why: closed split panes mint fresh leaf ids when reopened. Once their
|
||||
// retained binding expires, keeping the boundary stamp would leak one map
|
||||
// entry per closed pane for the renderer's lifetime.
|
||||
if (!liveTabIds.has(tabId) || !ptyBindingByPaneKey.has(paneKey)) {
|
||||
boundaryResolvedAtByPaneKey.delete(paneKey)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user