perf(sidebar): share one worktree-keyed agent orchestration index (#10678)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
a84dd33afb
commit
4ff2f51782
|
|
@ -403,15 +403,22 @@ describe('selectRuntimeAgentOrchestrationBatch', () => {
|
|||
)
|
||||
|
||||
expect(Object.keys(actual)).toEqual(Object.keys(expected))
|
||||
const operationBudget = {
|
||||
// Why the batch stays tighter: it knows which worktrees are on screen. The
|
||||
// shared index covers all of them, so it saves per *card*, not per worktree.
|
||||
expect(batched.counts()).toEqual({
|
||||
runtimeEnumerations: 1,
|
||||
runtimeValueReads: contextCount,
|
||||
contextVisits: contextCount,
|
||||
targetTabIdReads: 1,
|
||||
unrelatedTabIdReads: 0
|
||||
}
|
||||
expect(reference.counts()).toEqual(operationBudget)
|
||||
expect(batched.counts()).toEqual(operationBudget)
|
||||
})
|
||||
expect(reference.counts()).toEqual({
|
||||
runtimeEnumerations: 1,
|
||||
runtimeValueReads: contextCount,
|
||||
contextVisits: contextCount,
|
||||
targetTabIdReads: 1,
|
||||
unrelatedTabIdReads: tabCount - 1
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses multi-worktree runtime scans and caches unchanged publications', () => {
|
||||
|
|
@ -478,10 +485,12 @@ describe('selectRuntimeAgentOrchestrationBatch', () => {
|
|||
}
|
||||
selectRuntimeAgentOrchestrationBatch(batched.state, requested)
|
||||
|
||||
// One enumeration for worktreeCount calls: the first builds the shared
|
||||
// index, the rest are Map lookups. This used to scale with mounted cards.
|
||||
expect(reference.counts()).toEqual({
|
||||
runtimeEnumerations: worktreeCount,
|
||||
runtimeValueReads: worktreeCount * contextCount,
|
||||
contextVisits: worktreeCount * contextCount,
|
||||
runtimeEnumerations: 1,
|
||||
runtimeValueReads: contextCount,
|
||||
contextVisits: contextCount,
|
||||
tabIdReads: worktreeCount
|
||||
})
|
||||
expect(batched.counts()).toEqual({
|
||||
|
|
@ -518,5 +527,42 @@ describe('selectRuntimeAgentOrchestrationBatch', () => {
|
|||
contextVisits: contextCount * (publicationCount + 1),
|
||||
tabIdReads: worktreeCount
|
||||
})
|
||||
|
||||
// Publications that change nothing the index reads cost nothing, however
|
||||
// many cards call in.
|
||||
const referenceBefore = reference.counts()
|
||||
for (let publication = 0; publication < publicationCount; publication += 1) {
|
||||
for (const worktreeId of requested) {
|
||||
selectRuntimeAgentOrchestrationForWorktree(reference.state, worktreeId)
|
||||
}
|
||||
}
|
||||
expect(reference.counts()).toEqual(referenceBefore)
|
||||
|
||||
// Why this is the honest claim: a real live-status ping replaces
|
||||
// agentStatusByPaneKey, so the index does rebuild once per publication. What
|
||||
// the shared index removes is the mounted-card multiplier, not the
|
||||
// per-publication rebuild. Tab reads stay flat because tab membership is
|
||||
// keyed on the tabs slice, which a live-status ping does not replace.
|
||||
const churn = makeCountedState()
|
||||
for (const worktreeId of requested) {
|
||||
selectRuntimeAgentOrchestrationForWorktree(churn.state, worktreeId)
|
||||
}
|
||||
for (let publication = 0; publication < publicationCount; publication += 1) {
|
||||
const published = {
|
||||
...churn.state,
|
||||
agentStatusByPaneKey: {
|
||||
[`unrelated-${publication}`]: makeEntry(`unrelated-${publication}`, 'elsewhere')
|
||||
}
|
||||
}
|
||||
for (const worktreeId of requested) {
|
||||
selectRuntimeAgentOrchestrationForWorktree(published, worktreeId)
|
||||
}
|
||||
}
|
||||
expect(churn.counts()).toEqual({
|
||||
runtimeEnumerations: 1,
|
||||
runtimeValueReads: contextCount,
|
||||
contextVisits: contextCount * (publicationCount + 1),
|
||||
tabIdReads: worktreeCount
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,13 @@ const EMPTY_AGENT_STATUS: RuntimeOrchestrationState['agentStatusByPaneKey'] = {}
|
|||
const EMPTY_RETAINED_AGENTS: RuntimeOrchestrationState['retainedAgentsByPaneKey'] = {}
|
||||
const EMPTY_BATCH: ReadonlyMap<string, RuntimeOrchestrationRecord> = new Map()
|
||||
|
||||
export const EMPTY_WORKTREE_AGENT_ORCHESTRATION: RuntimeOrchestrationRecord = {}
|
||||
export const EMPTY_WORKTREE_AGENT_ORCHESTRATION: RuntimeOrchestrationRecord = Object.freeze({})
|
||||
|
||||
// Why null-prototype: a pane key of `__proto__` is a plain data key here; on a
|
||||
// normal object the write vanishes into the prototype setter and repoints it.
|
||||
function createRecord(): RuntimeOrchestrationRecord {
|
||||
return Object.create(null) as RuntimeOrchestrationRecord
|
||||
}
|
||||
|
||||
let runtimeDomainCache: RuntimeDomainCache | null = null
|
||||
let requestedTabMembershipCache: RequestedTabMembershipCache | null = null
|
||||
|
|
@ -181,12 +187,12 @@ function buildRuntimeBatch(
|
|||
}
|
||||
|
||||
for (const worktreeId of targets) {
|
||||
const existing = recordsByWorktree.get(worktreeId)
|
||||
if (existing) {
|
||||
existing[paneKey] = orchestration
|
||||
} else {
|
||||
recordsByWorktree.set(worktreeId, { [paneKey]: orchestration })
|
||||
let record = recordsByWorktree.get(worktreeId)
|
||||
if (!record) {
|
||||
record = createRecord()
|
||||
recordsByWorktree.set(worktreeId, record)
|
||||
}
|
||||
record[paneKey] = orchestration
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,430 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type {
|
||||
AgentStatusEntry,
|
||||
AgentStatusOrchestrationContext
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import type { RetainedAgentEntry } from '@/store/slices/agent-status'
|
||||
import type { TerminalTab } from '../../../../shared/types'
|
||||
import { makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import {
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION,
|
||||
releaseWorktreeAgentOrchestrationIndexCache,
|
||||
selectWorktreeAgentOrchestration
|
||||
} from './worktree-agent-orchestration-index'
|
||||
|
||||
type IndexState = Parameters<typeof selectWorktreeAgentOrchestration>[0]
|
||||
|
||||
const EMPTY_RECORD = {}
|
||||
|
||||
// Pane keys that reach this selector unvalidated. `__proto__` is the one whose
|
||||
// meaning depends on how the output record is built.
|
||||
const MALFORMED_RUNTIME_KEYS = ['__proto__', 'constructor', 'toString', 'no-colon', 'a:b:c']
|
||||
|
||||
// Why not plain assignment: writing `__proto__` onto an object literal hits the
|
||||
// prototype setter, so the fixture itself would lose the key under test.
|
||||
function defineKey<T>(map: Record<string, T>, key: string, value: T): void {
|
||||
Object.defineProperty(map, key, { value, enumerable: true, writable: true, configurable: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* The pre-index per-card selector, transcribed from the revision this index
|
||||
* replaced. Kept as the oracle so equivalence is asserted against real prior
|
||||
* behavior rather than against a restatement of the new implementation.
|
||||
*
|
||||
* One deliberate correction: the original accumulated into `{}`, so a pane key
|
||||
* of `__proto__` hit the prototype setter and vanished. This builds a
|
||||
* null-prototype record so the oracle expresses intended attribution.
|
||||
*/
|
||||
function legacySelectForWorktree(
|
||||
state: IndexState,
|
||||
worktreeId: string
|
||||
): Record<string, AgentStatusOrchestrationContext> {
|
||||
const tabs = (state.tabsByWorktree ?? EMPTY_RECORD)[worktreeId] ?? []
|
||||
const tabIds = new Set(tabs.map((tab) => tab.id))
|
||||
const out: Record<string, AgentStatusOrchestrationContext> = Object.create(null)
|
||||
const runtimeAgentOrchestrationByPaneKey =
|
||||
state.runtimeAgentOrchestrationByPaneKey ?? EMPTY_RECORD
|
||||
const agentStatusByPaneKey = state.agentStatusByPaneKey ?? EMPTY_RECORD
|
||||
const retainedAgentsByPaneKey = state.retainedAgentsByPaneKey ?? EMPTY_RECORD
|
||||
for (const [paneKey, orchestration] of Object.entries(runtimeAgentOrchestrationByPaneKey)) {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const parsedParent = orchestration.parentPaneKey
|
||||
? parsePaneKey(orchestration.parentPaneKey)
|
||||
: null
|
||||
const liveEntry = agentStatusByPaneKey[paneKey]
|
||||
const retainedEntry = retainedAgentsByPaneKey[paneKey]
|
||||
if (
|
||||
(parsed && tabIds.has(parsed.tabId)) ||
|
||||
(parsedParent && tabIds.has(parsedParent.tabId)) ||
|
||||
liveEntry?.worktreeId === worktreeId ||
|
||||
retainedEntry?.worktreeId === worktreeId
|
||||
) {
|
||||
out[paneKey] = orchestration
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Why a seeded PRNG: a randomized differential must reproduce exactly when it
|
||||
// reports a divergence.
|
||||
function createRandom(seed: number): () => number {
|
||||
// Why the multiply: xorshift32 needs a high-entropy state. Seeding it with a
|
||||
// small integer keeps the first output under 0.019, so `1 + pick(5)` was 1 for
|
||||
// every seed here and the suite only ever built single-worktree stores.
|
||||
let state = Math.imul(seed >>> 0 || 1, 0x9e37_79b1) >>> 0 || 1
|
||||
return () => {
|
||||
state ^= state << 13
|
||||
state >>>= 0
|
||||
state ^= state >> 17
|
||||
state ^= state << 5
|
||||
state >>>= 0
|
||||
return state / 0x1_00_00_00_00
|
||||
}
|
||||
}
|
||||
|
||||
function makeTab(id: string): TerminalTab {
|
||||
return {
|
||||
id,
|
||||
worktreeId: 'unused',
|
||||
ptyId: null,
|
||||
title: 'Claude',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
function paneKeyFor(tabId: string, index: number): string {
|
||||
return makePaneKey(tabId, `88888888-8888-4888-8888-${index.toString(16).padStart(12, '0')}`)
|
||||
}
|
||||
|
||||
function makeEntry(paneKey: string, worktreeId: string): AgentStatusEntry {
|
||||
return { paneKey, worktreeId, state: 'busy', startedAt: 0 } as unknown as AgentStatusEntry
|
||||
}
|
||||
|
||||
function makeRetained(paneKey: string, worktreeId: string): RetainedAgentEntry {
|
||||
return {
|
||||
worktreeId,
|
||||
entry: makeEntry(paneKey, worktreeId),
|
||||
retainedAt: 0
|
||||
} as unknown as RetainedAgentEntry
|
||||
}
|
||||
|
||||
describe('selectWorktreeAgentOrchestration', () => {
|
||||
beforeEach(() => {
|
||||
releaseWorktreeAgentOrchestrationIndexCache()
|
||||
})
|
||||
|
||||
it('matches the pre-index per-card selector across randomized stores', () => {
|
||||
for (let seed = 1; seed <= 300; seed += 1) {
|
||||
releaseWorktreeAgentOrchestrationIndexCache()
|
||||
const random = createRandom(seed)
|
||||
const pick = (limit: number): number => Math.floor(random() * limit)
|
||||
|
||||
const worktreeCount = 1 + pick(5)
|
||||
const worktreeIds = Array.from({ length: worktreeCount }, (_, index) => `wt-${index}`)
|
||||
const tabsByWorktree: Record<string, TerminalTab[]> = {}
|
||||
const tabIds: string[] = []
|
||||
for (const worktreeId of worktreeIds) {
|
||||
const tabCount = pick(3)
|
||||
const tabs: TerminalTab[] = []
|
||||
for (let tabIndex = 0; tabIndex < tabCount; tabIndex += 1) {
|
||||
// Why a small shared id space: tab ids collide across worktrees on
|
||||
// 131 of these 300 seeds, which is the multi-attribution path.
|
||||
const tabId = `tab-${pick(4)}`
|
||||
tabs.push(makeTab(tabId))
|
||||
tabIds.push(tabId)
|
||||
}
|
||||
tabsByWorktree[worktreeId] = tabs
|
||||
}
|
||||
|
||||
const runtimeAgentOrchestrationByPaneKey: Record<string, AgentStatusOrchestrationContext> = {}
|
||||
const agentStatusByPaneKey: Record<string, AgentStatusEntry> = {}
|
||||
const retainedAgentsByPaneKey: Record<string, RetainedAgentEntry> = {}
|
||||
const contextCount = pick(8)
|
||||
for (let index = 0; index < contextCount; index += 1) {
|
||||
const ownerTabId =
|
||||
random() < 0.7 && tabIds.length > 0 ? tabIds[pick(tabIds.length)] : 'tab-orphan'
|
||||
// Why malformed runtime keys: a pane key that is not `tab:uuid` reaches
|
||||
// this selector unvalidated, and `__proto__` is the one that changes
|
||||
// meaning depending on how the output record is built.
|
||||
const keyRoll = random()
|
||||
const paneKey =
|
||||
keyRoll < 0.08
|
||||
? MALFORMED_RUNTIME_KEYS[pick(MALFORMED_RUNTIME_KEYS.length)]
|
||||
: paneKeyFor(ownerTabId, index)
|
||||
const parentRoll = random()
|
||||
const parentPaneKey =
|
||||
parentRoll < 0.3 && tabIds.length > 0
|
||||
? paneKeyFor(tabIds[pick(tabIds.length)], 900 + index)
|
||||
: parentRoll < 0.4
|
||||
? 'malformed:parent:key'
|
||||
: undefined
|
||||
defineKey(runtimeAgentOrchestrationByPaneKey, paneKey, {
|
||||
taskId: `task-${index}`,
|
||||
dispatchId: `dispatch-${index}`,
|
||||
...(parentPaneKey === undefined ? {} : { parentPaneKey })
|
||||
})
|
||||
if (random() < 0.35) {
|
||||
defineKey(
|
||||
agentStatusByPaneKey,
|
||||
paneKey,
|
||||
makeEntry(paneKey, `wt-${pick(worktreeCount + 1)}`)
|
||||
)
|
||||
}
|
||||
if (random() < 0.25) {
|
||||
defineKey(
|
||||
retainedAgentsByPaneKey,
|
||||
paneKey,
|
||||
makeRetained(paneKey, `wt-${pick(worktreeCount + 1)}`)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
tabsByWorktree,
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey
|
||||
} as unknown as IndexState
|
||||
|
||||
// Why the extra id: worktrees with no tabs are still reachable through a
|
||||
// live or retained attribution, and must resolve identically.
|
||||
for (const worktreeId of [...worktreeIds, `wt-${worktreeCount}`, 'missing']) {
|
||||
const expected = legacySelectForWorktree(state, worktreeId)
|
||||
const actual = selectWorktreeAgentOrchestration(state, worktreeId)
|
||||
expect(Object.keys(actual), `seed ${seed} / ${worktreeId}`).toEqual(Object.keys(expected))
|
||||
for (const paneKey of Object.keys(expected)) {
|
||||
expect(actual[paneKey], `seed ${seed} / ${worktreeId} / ${paneKey}`).toBe(
|
||||
expected[paneKey]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('returns one shared empty record for worktrees with no orchestration', () => {
|
||||
const state = {
|
||||
tabsByWorktree: { 'wt-1': [makeTab('tab-1')] },
|
||||
runtimeAgentOrchestrationByPaneKey: {
|
||||
[paneKeyFor('tab-1', 0)]: { taskId: 't', dispatchId: 'd' }
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
expect(selectWorktreeAgentOrchestration(state, 'wt-2')).toBe(EMPTY_WORKTREE_AGENT_ORCHESTRATION)
|
||||
expect(selectWorktreeAgentOrchestration(state, 'wt-2')).toBe(
|
||||
selectWorktreeAgentOrchestration(state, 'wt-3')
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps record identity stable across publications that change nothing it reads', () => {
|
||||
const context = { taskId: 't', dispatchId: 'd' }
|
||||
const base = {
|
||||
tabsByWorktree: { 'wt-1': [makeTab('tab-1')] },
|
||||
runtimeAgentOrchestrationByPaneKey: { [paneKeyFor('tab-1', 0)]: context },
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
const first = selectWorktreeAgentOrchestration(base, 'wt-1')
|
||||
expect(selectWorktreeAgentOrchestration({ ...base } as IndexState, 'wt-1')).toBe(first)
|
||||
|
||||
// Why: a live-status ping for an unrelated pane is the highest-frequency
|
||||
// publication in this store, and must not hand cards a new object.
|
||||
const liveChurn = {
|
||||
...base,
|
||||
agentStatusByPaneKey: { unrelated: makeEntry('unrelated', 'wt-9') }
|
||||
} as unknown as IndexState
|
||||
expect(selectWorktreeAgentOrchestration(liveChurn, 'wt-1')).toBe(first)
|
||||
|
||||
const retainedChurn = {
|
||||
...liveChurn,
|
||||
retainedAgentsByPaneKey: { unrelated: makeRetained('unrelated', 'wt-9') }
|
||||
} as unknown as IndexState
|
||||
expect(selectWorktreeAgentOrchestration(retainedChurn, 'wt-1')).toBe(first)
|
||||
})
|
||||
|
||||
it('rebuilds when a source it reads actually changes', () => {
|
||||
const context = { taskId: 't', dispatchId: 'd' }
|
||||
const paneKey = paneKeyFor('tab-1', 0)
|
||||
const base = {
|
||||
tabsByWorktree: { 'wt-1': [makeTab('tab-1')] },
|
||||
runtimeAgentOrchestrationByPaneKey: { [paneKey]: context },
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
selectWorktreeAgentOrchestration(base, 'wt-1')
|
||||
|
||||
const replacement = { taskId: 't2', dispatchId: 'd2' }
|
||||
const replaced = {
|
||||
...base,
|
||||
runtimeAgentOrchestrationByPaneKey: { [paneKey]: replacement }
|
||||
} as unknown as IndexState
|
||||
expect(selectWorktreeAgentOrchestration(replaced, 'wt-1')[paneKey]).toBe(replacement)
|
||||
|
||||
// Why: moving the tab to another worktree must re-attribute, which only
|
||||
// happens if tab membership is keyed on the tabs slice identity.
|
||||
const movedTab = {
|
||||
...replaced,
|
||||
tabsByWorktree: { 'wt-2': [makeTab('tab-1')] }
|
||||
} as unknown as IndexState
|
||||
expect(selectWorktreeAgentOrchestration(movedTab, 'wt-1')).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
expect(selectWorktreeAgentOrchestration(movedTab, 'wt-2')[paneKey]).toBe(replacement)
|
||||
})
|
||||
|
||||
it('treats a missing or emptied orchestration map as empty without reading other slices', () => {
|
||||
let forbiddenReads = 0
|
||||
const coldState = {
|
||||
runtimeAgentOrchestrationByPaneKey: {},
|
||||
get tabsByWorktree() {
|
||||
forbiddenReads += 1
|
||||
return {}
|
||||
},
|
||||
get agentStatusByPaneKey() {
|
||||
forbiddenReads += 1
|
||||
return {}
|
||||
},
|
||||
get retainedAgentsByPaneKey() {
|
||||
forbiddenReads += 1
|
||||
return {}
|
||||
}
|
||||
} as unknown as IndexState
|
||||
|
||||
expect(selectWorktreeAgentOrchestration(coldState, 'wt-1')).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
expect(selectWorktreeAgentOrchestration({} as IndexState, 'wt-1')).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
expect(forbiddenReads).toBe(0)
|
||||
})
|
||||
|
||||
it('does not attribute unowned panes to a nullish worktree id', () => {
|
||||
const paneKey = paneKeyFor('tab-orphan', 0)
|
||||
const state = {
|
||||
tabsByWorktree: {},
|
||||
runtimeAgentOrchestrationByPaneKey: { [paneKey]: { taskId: 't', dispatchId: 'd' } },
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
// Why asserted despite `worktreeId: string`: the pre-index selector tested
|
||||
// `entry?.worktreeId === worktreeId`, so a nullish id matched every pane
|
||||
// that had no live/retained entry. This is the one intentional divergence
|
||||
// from the oracle — the old result was wrong, not merely different.
|
||||
expect(selectWorktreeAgentOrchestration(state, undefined as unknown as string)).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
expect(legacySelectForWorktree(state, undefined as unknown as string)).toHaveProperty(paneKey)
|
||||
})
|
||||
|
||||
it('stays correct when two stores interleave through the single cache slot', () => {
|
||||
// Why: the cache is one module-level slot, but the sidebar cards and the
|
||||
// dashboard snapshot can call in with different state objects. Thrashing may
|
||||
// cost a rebuild; it must never return another store's answer.
|
||||
const buildState = (suffix: string): IndexState =>
|
||||
({
|
||||
tabsByWorktree: { [`wt-${suffix}`]: [makeTab(`tab-${suffix}`)] },
|
||||
runtimeAgentOrchestrationByPaneKey: {
|
||||
[paneKeyFor(`tab-${suffix}`, 0)]: { taskId: `t-${suffix}`, dispatchId: `d-${suffix}` }
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
}) as unknown as IndexState
|
||||
|
||||
const stateA = buildState('a')
|
||||
const stateB = buildState('b')
|
||||
for (let round = 0; round < 4; round += 1) {
|
||||
for (const [state, suffix] of [
|
||||
[stateA, 'a'],
|
||||
[stateB, 'b']
|
||||
] as const) {
|
||||
expect(Object.keys(selectWorktreeAgentOrchestration(state, `wt-${suffix}`))).toEqual(
|
||||
Object.keys(legacySelectForWorktree(state, `wt-${suffix}`))
|
||||
)
|
||||
// The other store's worktree must never leak through the shared slot.
|
||||
const foreign = suffix === 'a' ? 'wt-b' : 'wt-a'
|
||||
expect(selectWorktreeAgentOrchestration(state, foreign)).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('treats a __proto__ pane key as data instead of a prototype write', () => {
|
||||
// Why: writing this key into a normal object silently drops the entry and
|
||||
// repoints the record's prototype at the orchestration context.
|
||||
const context = { taskId: 't', dispatchId: 'd' }
|
||||
const state = {
|
||||
tabsByWorktree: {},
|
||||
runtimeAgentOrchestrationByPaneKey: Object.fromEntries([['__proto__', context]]),
|
||||
agentStatusByPaneKey: Object.fromEntries([['__proto__', makeEntry('__proto__', 'wt-1')]]),
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
const record = selectWorktreeAgentOrchestration(state, 'wt-1')
|
||||
expect(Object.keys(record)).toEqual(['__proto__'])
|
||||
expect(record['__proto__']).toBe(context)
|
||||
expect(Object.getPrototypeOf(record)).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the entries cache warm while the orchestration map is empty', () => {
|
||||
// Why: the empty map is the common case, so re-enumerating it per card is
|
||||
// exactly the per-publication cost this index exists to remove.
|
||||
let enumerations = 0
|
||||
const runtimeAgentOrchestrationByPaneKey = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys(target) {
|
||||
enumerations += 1
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
}
|
||||
)
|
||||
const state = {
|
||||
tabsByWorktree: {},
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
for (const worktreeId of ['wt-a', 'wt-b', 'wt-c']) {
|
||||
expect(selectWorktreeAgentOrchestration(state, worktreeId)).toBe(
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
}
|
||||
expect(enumerations).toBe(1)
|
||||
})
|
||||
|
||||
it('never mutates a record already handed to a subscriber', () => {
|
||||
// Why: buildIndex fills record objects in place and can hand back a previous
|
||||
// build's record. A mounted card holds that object across renders, so a later
|
||||
// build writing into it would break React's snapshot contract silently.
|
||||
const paneKey = paneKeyFor('tab-1', 0)
|
||||
const base = {
|
||||
tabsByWorktree: { 'wt-1': [makeTab('tab-1')] },
|
||||
runtimeAgentOrchestrationByPaneKey: { [paneKey]: { taskId: 't', dispatchId: 'd' } },
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
} as unknown as IndexState
|
||||
|
||||
const held = selectWorktreeAgentOrchestration(base, 'wt-1')
|
||||
const heldKeys = Object.keys(held)
|
||||
|
||||
const secondPaneKey = paneKeyFor('tab-1', 1)
|
||||
const grown = {
|
||||
...base,
|
||||
runtimeAgentOrchestrationByPaneKey: {
|
||||
...base.runtimeAgentOrchestrationByPaneKey,
|
||||
[secondPaneKey]: { taskId: 't2', dispatchId: 'd2' }
|
||||
}
|
||||
} as unknown as IndexState
|
||||
expect(Object.keys(selectWorktreeAgentOrchestration(grown, 'wt-1'))).toHaveLength(2)
|
||||
expect(Object.keys(held)).toEqual(heldKeys)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import type { AppState } from '@/store/types'
|
||||
import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
|
||||
type OrchestrationIndexState = Pick<
|
||||
AppState,
|
||||
| 'agentStatusByPaneKey'
|
||||
| 'retainedAgentsByPaneKey'
|
||||
| 'runtimeAgentOrchestrationByPaneKey'
|
||||
| 'tabsByWorktree'
|
||||
>
|
||||
|
||||
type RuntimeOrchestrationRecord = Record<string, AgentStatusOrchestrationContext>
|
||||
|
||||
type RuntimeEntriesCache = {
|
||||
source: OrchestrationIndexState['runtimeAgentOrchestrationByPaneKey']
|
||||
entries: [string, AgentStatusOrchestrationContext][]
|
||||
}
|
||||
|
||||
type TabMembershipCache = {
|
||||
tabsSource: OrchestrationIndexState['tabsByWorktree']
|
||||
worktreeIdsByTabId: Map<string, Set<string>>
|
||||
}
|
||||
|
||||
type OrchestrationIndexCache = {
|
||||
runtimeSource: OrchestrationIndexState['runtimeAgentOrchestrationByPaneKey']
|
||||
tabsSource: OrchestrationIndexState['tabsByWorktree']
|
||||
liveSource: OrchestrationIndexState['agentStatusByPaneKey']
|
||||
retainedSource: OrchestrationIndexState['retainedAgentsByPaneKey']
|
||||
recordsByWorktree: ReadonlyMap<string, RuntimeOrchestrationRecord>
|
||||
}
|
||||
|
||||
// Why: selector unit tests pass partial store mocks; a missing map must behave
|
||||
// like an empty slice while keeping a stable identity for the source cache.
|
||||
const EMPTY_SOURCE = {}
|
||||
|
||||
// Why frozen: these are shared by every card, so an accidental write would
|
||||
// corrupt unrelated worktrees rather than fail locally.
|
||||
export const EMPTY_WORKTREE_AGENT_ORCHESTRATION: RuntimeOrchestrationRecord = Object.freeze({})
|
||||
export const EMPTY_WORKTREE_AGENT_ORCHESTRATION_INDEX: ReadonlyMap<
|
||||
string,
|
||||
RuntimeOrchestrationRecord
|
||||
> = new Map()
|
||||
|
||||
// Why null-prototype: a pane key of `__proto__` is a plain data key here. On a
|
||||
// normal object the first write silently vanishes into the prototype setter,
|
||||
// which both drops the entry and repoints the record's prototype.
|
||||
function createRecord(): RuntimeOrchestrationRecord {
|
||||
return Object.create(null) as RuntimeOrchestrationRecord
|
||||
}
|
||||
|
||||
let runtimeEntriesCache: RuntimeEntriesCache | null = null
|
||||
let tabMembershipCache: TabMembershipCache | null = null
|
||||
let orchestrationIndexCache: OrchestrationIndexCache | null = null
|
||||
|
||||
export function releaseWorktreeAgentOrchestrationIndexCache(): void {
|
||||
runtimeEntriesCache = null
|
||||
tabMembershipCache = null
|
||||
orchestrationIndexCache = null
|
||||
}
|
||||
|
||||
function reuseRecordIfOrderedEqual(
|
||||
previous: RuntimeOrchestrationRecord | undefined,
|
||||
next: RuntimeOrchestrationRecord
|
||||
): RuntimeOrchestrationRecord {
|
||||
if (!previous) {
|
||||
return next
|
||||
}
|
||||
const previousEntries = Object.entries(previous)
|
||||
const nextEntries = Object.entries(next)
|
||||
if (previousEntries.length !== nextEntries.length) {
|
||||
return next
|
||||
}
|
||||
for (let index = 0; index < nextEntries.length; index += 1) {
|
||||
if (
|
||||
previousEntries[index]?.[0] !== nextEntries[index]?.[0] ||
|
||||
previousEntries[index]?.[1] !== nextEntries[index]?.[1]
|
||||
) {
|
||||
return next
|
||||
}
|
||||
}
|
||||
return previous
|
||||
}
|
||||
|
||||
function getWorktreeIdsByTabId(
|
||||
tabsByWorktree: OrchestrationIndexState['tabsByWorktree']
|
||||
): Map<string, Set<string>> {
|
||||
if (tabMembershipCache?.tabsSource === tabsByWorktree) {
|
||||
return tabMembershipCache.worktreeIdsByTabId
|
||||
}
|
||||
// Why a Set per tab: the same tab id can appear under more than one worktree,
|
||||
// and each of those worktrees must still see the pane's orchestration.
|
||||
const worktreeIdsByTabId = new Map<string, Set<string>>()
|
||||
for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
|
||||
for (const tab of tabs ?? []) {
|
||||
const tabId = tab.id
|
||||
const existing = worktreeIdsByTabId.get(tabId)
|
||||
if (existing) {
|
||||
existing.add(worktreeId)
|
||||
} else {
|
||||
worktreeIdsByTabId.set(tabId, new Set([worktreeId]))
|
||||
}
|
||||
}
|
||||
}
|
||||
tabMembershipCache = { tabsSource: tabsByWorktree, worktreeIdsByTabId }
|
||||
return worktreeIdsByTabId
|
||||
}
|
||||
|
||||
function buildIndex(
|
||||
runtimeEntries: [string, AgentStatusOrchestrationContext][],
|
||||
tabsByWorktree: OrchestrationIndexState['tabsByWorktree'],
|
||||
agentStatusByPaneKey: OrchestrationIndexState['agentStatusByPaneKey'],
|
||||
retainedAgentsByPaneKey: OrchestrationIndexState['retainedAgentsByPaneKey']
|
||||
): ReadonlyMap<string, RuntimeOrchestrationRecord> {
|
||||
const worktreeIdsByTabId = getWorktreeIdsByTabId(tabsByWorktree)
|
||||
const recordsByWorktree = new Map<string, RuntimeOrchestrationRecord>()
|
||||
|
||||
for (const [paneKey, orchestration] of runtimeEntries) {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const parsedParent = orchestration.parentPaneKey
|
||||
? parsePaneKey(orchestration.parentPaneKey)
|
||||
: null
|
||||
const targets = new Set<string>()
|
||||
if (parsed) {
|
||||
for (const worktreeId of worktreeIdsByTabId.get(parsed.tabId) ?? []) {
|
||||
targets.add(worktreeId)
|
||||
}
|
||||
}
|
||||
// Why: child agent terminals can be attributed to a worktree before their
|
||||
// tab reaches this renderer, or after the row has been retained as done.
|
||||
// The parent link must still reach that worktree card.
|
||||
if (parsedParent) {
|
||||
for (const worktreeId of worktreeIdsByTabId.get(parsedParent.tabId) ?? []) {
|
||||
targets.add(worktreeId)
|
||||
}
|
||||
}
|
||||
// Why exact runtime keys: this preserves early SSH attribution and ignores
|
||||
// stale entry.paneKey fields carried by a live or retained row.
|
||||
const liveWorktreeId = agentStatusByPaneKey[paneKey]?.worktreeId
|
||||
if (typeof liveWorktreeId === 'string') {
|
||||
targets.add(liveWorktreeId)
|
||||
}
|
||||
const retainedWorktreeId = retainedAgentsByPaneKey[paneKey]?.worktreeId
|
||||
if (typeof retainedWorktreeId === 'string') {
|
||||
targets.add(retainedWorktreeId)
|
||||
}
|
||||
|
||||
for (const worktreeId of targets) {
|
||||
let record = recordsByWorktree.get(worktreeId)
|
||||
if (!record) {
|
||||
record = createRecord()
|
||||
recordsByWorktree.set(worktreeId, record)
|
||||
}
|
||||
record[paneKey] = orchestration
|
||||
}
|
||||
}
|
||||
|
||||
const previousRecords = orchestrationIndexCache?.recordsByWorktree
|
||||
for (const [worktreeId, record] of recordsByWorktree) {
|
||||
recordsByWorktree.set(
|
||||
worktreeId,
|
||||
reuseRecordIfOrderedEqual(previousRecords?.get(worktreeId), record)
|
||||
)
|
||||
}
|
||||
return recordsByWorktree
|
||||
}
|
||||
|
||||
/**
|
||||
* Worktree-keyed index of runtime agent orchestration contexts, rebuilt only
|
||||
* when one of its four source maps changes identity.
|
||||
*
|
||||
* Why: every mounted worktree card subscribes to its own orchestration slice,
|
||||
* and Zustand re-runs every subscriber's selector on every store publication.
|
||||
* Scanning the whole context map per card made that O(cards x contexts). What
|
||||
* this removes is the per-card multiplier, not the rebuild itself: an agent
|
||||
* ping replaces the live map, so the index still rebuilds once per publication.
|
||||
* The first caller through a given store version pays O(tabs + contexts); the
|
||||
* rest are a Map lookup.
|
||||
*/
|
||||
export function selectWorktreeAgentOrchestrationIndex(
|
||||
state: OrchestrationIndexState
|
||||
): ReadonlyMap<string, RuntimeOrchestrationRecord> {
|
||||
const runtimeAgentOrchestrationByPaneKey =
|
||||
state.runtimeAgentOrchestrationByPaneKey ?? EMPTY_SOURCE
|
||||
// Why cached separately from the index: enumerating the context map is the
|
||||
// per-publication cost this index exists to remove, and the entry list stays
|
||||
// valid even when a churning live/retained slice forces an index rebuild.
|
||||
if (runtimeEntriesCache?.source !== runtimeAgentOrchestrationByPaneKey) {
|
||||
runtimeEntriesCache = {
|
||||
source: runtimeAgentOrchestrationByPaneKey,
|
||||
entries: Object.entries(runtimeAgentOrchestrationByPaneKey)
|
||||
}
|
||||
}
|
||||
const runtimeEntries = runtimeEntriesCache.entries
|
||||
// Why here rather than before the enumeration: with no contexts the index is
|
||||
// empty whatever the other slices hold, and callers rely on them staying unread.
|
||||
if (runtimeEntries.length === 0) {
|
||||
// Why the entries cache survives: dropping it would re-enumerate the empty
|
||||
// map once per card, which is the per-publication cost this index removes.
|
||||
tabMembershipCache = null
|
||||
orchestrationIndexCache = null
|
||||
return EMPTY_WORKTREE_AGENT_ORCHESTRATION_INDEX
|
||||
}
|
||||
|
||||
const tabsByWorktree = state.tabsByWorktree ?? EMPTY_SOURCE
|
||||
const agentStatusByPaneKey = state.agentStatusByPaneKey ?? EMPTY_SOURCE
|
||||
const retainedAgentsByPaneKey = state.retainedAgentsByPaneKey ?? EMPTY_SOURCE
|
||||
if (
|
||||
orchestrationIndexCache?.runtimeSource === runtimeAgentOrchestrationByPaneKey &&
|
||||
orchestrationIndexCache.tabsSource === tabsByWorktree &&
|
||||
orchestrationIndexCache.liveSource === agentStatusByPaneKey &&
|
||||
orchestrationIndexCache.retainedSource === retainedAgentsByPaneKey
|
||||
) {
|
||||
return orchestrationIndexCache.recordsByWorktree
|
||||
}
|
||||
|
||||
orchestrationIndexCache = {
|
||||
runtimeSource: runtimeAgentOrchestrationByPaneKey,
|
||||
tabsSource: tabsByWorktree,
|
||||
liveSource: agentStatusByPaneKey,
|
||||
retainedSource: retainedAgentsByPaneKey,
|
||||
recordsByWorktree: buildIndex(
|
||||
runtimeEntries,
|
||||
tabsByWorktree,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey
|
||||
)
|
||||
}
|
||||
return orchestrationIndexCache.recordsByWorktree
|
||||
}
|
||||
|
||||
export function selectWorktreeAgentOrchestration(
|
||||
state: OrchestrationIndexState,
|
||||
worktreeId: string
|
||||
): RuntimeOrchestrationRecord {
|
||||
return (
|
||||
selectWorktreeAgentOrchestrationIndex(state).get(worktreeId) ??
|
||||
EMPTY_WORKTREE_AGENT_ORCHESTRATION
|
||||
)
|
||||
}
|
||||
|
|
@ -12,12 +12,12 @@ import {
|
|||
patchLiveEntriesByWorktree,
|
||||
recordLiveEntriesFullRebuild
|
||||
} from './worktree-agent-live-index-patch'
|
||||
import { selectWorktreeAgentOrchestration } from './worktree-agent-orchestration-index'
|
||||
import type { TerminalLayoutSnapshot } from '../../../../shared/types'
|
||||
|
||||
const EMPTY_LIVE_ENTRIES: AgentStatusEntry[] = []
|
||||
const EMPTY_MIGRATION_UNSUPPORTED_ENTRIES: MigrationUnsupportedPtyEntry[] = []
|
||||
const EMPTY_RETAINED: RetainedAgentEntry[] = []
|
||||
const EMPTY_RUNTIME_AGENT_ORCHESTRATION: Record<string, AgentStatusOrchestrationContext> = {}
|
||||
// Why: selector unit tests often pass partial store mocks; production state
|
||||
// owns these maps, but missing mock maps should behave like empty slices.
|
||||
const EMPTY_RECORD = {}
|
||||
|
|
@ -227,6 +227,10 @@ export function selectRetainedAgentEntriesForWorktree(
|
|||
return getRetainedEntriesByWorktree(state).get(worktreeId) ?? EMPTY_RETAINED
|
||||
}
|
||||
|
||||
// Why: reads a shared worktree-keyed index instead of rescanning every
|
||||
// orchestration context. Zustand re-runs each mounted card's selector on every
|
||||
// publication, so the old per-card scan was O(cards x contexts) on unrelated
|
||||
// traffic; only the first card through a given store version now pays a build.
|
||||
export function selectRuntimeAgentOrchestrationForWorktree(
|
||||
state: Pick<
|
||||
AppState,
|
||||
|
|
@ -237,33 +241,7 @@ export function selectRuntimeAgentOrchestrationForWorktree(
|
|||
>,
|
||||
worktreeId: string
|
||||
): Record<string, AgentStatusOrchestrationContext> {
|
||||
const tabs = (state.tabsByWorktree ?? EMPTY_RECORD)[worktreeId] ?? []
|
||||
const tabIds = new Set(tabs.map((tab) => tab.id))
|
||||
const out: Record<string, AgentStatusOrchestrationContext> = {}
|
||||
const runtimeAgentOrchestrationByPaneKey =
|
||||
state.runtimeAgentOrchestrationByPaneKey ?? EMPTY_RECORD
|
||||
const agentStatusByPaneKey = state.agentStatusByPaneKey ?? EMPTY_RECORD
|
||||
const retainedAgentsByPaneKey = state.retainedAgentsByPaneKey ?? EMPTY_RECORD
|
||||
for (const [paneKey, orchestration] of Object.entries(runtimeAgentOrchestrationByPaneKey)) {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const parsedParent = orchestration.parentPaneKey
|
||||
? parsePaneKey(orchestration.parentPaneKey)
|
||||
: null
|
||||
const liveEntry = agentStatusByPaneKey[paneKey]
|
||||
const retainedEntry = retainedAgentsByPaneKey[paneKey]
|
||||
// Why: child agent terminals can be attributed to a worktree before their
|
||||
// tab reaches this renderer, or after the row has been retained as done.
|
||||
// The parent link must still reach that worktree card.
|
||||
if (
|
||||
(parsed && tabIds.has(parsed.tabId)) ||
|
||||
(parsedParent && tabIds.has(parsedParent.tabId)) ||
|
||||
liveEntry?.worktreeId === worktreeId ||
|
||||
retainedEntry?.worktreeId === worktreeId
|
||||
) {
|
||||
out[paneKey] = orchestration
|
||||
}
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : EMPTY_RUNTIME_AGENT_ORCHESTRATION
|
||||
return selectWorktreeAgentOrchestration(state, worktreeId)
|
||||
}
|
||||
|
||||
export function selectTerminalLayoutsForWorktree(
|
||||
|
|
|
|||
Loading…
Reference in New Issue