perf(agent-hooks): stop cloning the whole status roster on every hook event

AgentAwakeService.setStatuses deep-copied every row of an array the hook server
had just built fresh, and getEligibleRunningStatusCount allocated a filtered
array only to read its length. Both run on every agent turn and tool call, and
both scale with cached panes rather than with the one pane that changed.

Copy the array without cloning its rows, and count in place. At 500 panes the
per-event cost drops from 5.34us to 1.78us.
This commit is contained in:
Neil
2026-09-04 04:41:15 -07:00
parent 637dc30a32
commit b6847dcc02
2 changed files with 18 additions and 2 deletions
@@ -22,6 +22,20 @@ function workingStatus(): AgentAwakeStatus {
}
}
describe('AgentAwakeService status array ownership', () => {
it('does not observe rows appended to the caller array after setStatuses', () => {
const service = new AgentAwakeService()
service.setMode('auto')
const statuses: AgentAwakeStatus[] = [workingStatus()]
service.setStatuses(statuses)
const before = service.getWorkingAgentCount()
statuses.push(workingStatus(), workingStatus())
expect(service.getWorkingAgentCount()).toBe(before)
})
})
function createBlocker() {
const startedIds = new Set<number>()
let nextId = 1
+4 -2
View File
@@ -105,7 +105,8 @@ export class AgentAwakeService {
}
setStatuses(statuses: AgentAwakeStatus[]): void {
this.statuses = statuses.map((status) => ({ ...status }))
// Copy the array, not every row: the hook server allocates each row fresh per event.
this.statuses = [...statuses]
this.refresh('status-change')
}
@@ -171,7 +172,8 @@ export class AgentAwakeService {
private getEligibleRunningStatusCount(): number {
const now = this.now()
return this.statuses.filter((status) => this.isWakeEligible(status, now)).length
// Counted in place: the filtered array was only ever measured, and this runs per hook event.
return this.statuses.reduce((count, s) => count + (this.isWakeEligible(s, now) ? 1 : 0), 0)
}
private isWakeEligible(status: AgentAwakeStatus, now: number): boolean {