refactor(native-chat): the status summary's broadcast equality gets its own module

The status feed crossed the file-size limit once the summary gained the main agent's turn
outcome beside the child views. Which summary changes reach every session list now lives in
structured-agent-session-status-summary-equality.ts.
This commit is contained in:
Brennan Benson
2026-09-29 00:59:48 -07:00
parent 2ece5c204d
commit abfce6d769
2 changed files with 62 additions and 51 deletions
@@ -10,28 +10,24 @@
// each chat is reopened. Restart is the one boundary that forgets, and restoring readable sessions
// republishes them.
import { agentProviderSessionsEqual } from '../../../shared/agent-session-resume'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import { normalizeOptionalField } from '../../../shared/agent-status-field-normalization'
import { isAgentStatusHeldOpenByChildWork } from '../../../shared/agent-lead-status-fold'
import { AGENT_MODEL_MAX_LENGTH } from '../../../shared/agent-status-types'
import {
agentSessionBackgroundTasksEqual,
type AgentSessionStatusEvent,
type AgentSessionStatusSummary
import type {
AgentSessionStatusEvent,
AgentSessionStatusSummary
} from '../../../shared/agent-session-wire'
import type { AgentChildWorkEvidence } from '../../../shared/agent-status-child-work-evidence'
import type { AgentChildWorkView } from '../../../shared/agent-status-child-work-view'
import { projectStructuredAgentSessionStatusState } from '../../../shared/structured-agent-session-projection'
import { structuredAgentSessionAgentStatus } from '../../../shared/structured-agent-session-agent-status'
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
import type { StructuredAgentSessionProviderChild } from './structured-agent-session-host-types'
import { structuredAgentSessionProviderSessionMetadata } from './structured-agent-session-history-result'
import {
newestRootTurnId,
structuredStatusChildrenEqual,
structuredStatusChildWork
} from './structured-agent-session-status-child-work'
import { structuredStatusSummariesEqual } from './structured-agent-session-status-summary-equality'
import {
StructuredAgentSessionStatusOwnership,
type StructuredAgentSessionStatusSink
@@ -70,48 +66,6 @@ export type StructuredAgentSessionStatusFeedDeps = {
onAgentStarted?: (sessionId: string) => void
}
function summariesEqual(a: AgentSessionStatusSummary, b: AgentSessionStatusSummary): boolean {
return (
a.workspaceId === b.workspaceId &&
a.agent === b.agent &&
a.status === b.status &&
a.hostExecutionOwned === b.hostExecutionOwned &&
a.hostExecutionPhase === b.hostExecutionPhase &&
a.hostExecutionChild?.generation === b.hostExecutionChild?.generation &&
a.hostExecutionChild?.fence === b.hostExecutionChild?.fence &&
a.rewindBlockedReason === b.rewindBlockedReason &&
// A moved state clock changes ranking; row activity alone, including a subagent's, does not.
// An idle state the journal cannot date still republishes, since readers date it by `updatedAt`,
// and so does one live child work holds open: readers take each publish as its evidence.
a.statusStartedAt === b.statusStartedAt &&
(a.status !== 'idle' ||
a.updatedAt === b.updatedAt ||
(a.statusStartedAt !== undefined && !isIdleHeldOpenByChildWork(b))) &&
a.latestPrompt === b.latestPrompt &&
a.model === b.model &&
a.toolName === b.toolName &&
a.toolInput === b.toolInput &&
a.lastAssistantMessage === b.lastAssistantMessage &&
a.turnOutcome === b.turnOutcome &&
structuredStatusChildrenEqual(a.children, b.children) &&
agentSessionBackgroundTasksEqual(a.backgroundTasks, b.backgroundTasks) &&
agentProviderSessionsEqual(undefined, a.providerSession, b.providerSession)
)
}
function isIdleHeldOpenByChildWork(summary: AgentSessionStatusSummary): boolean {
return (
summary.status === 'idle' &&
isAgentStatusHeldOpenByChildWork(
structuredAgentSessionAgentStatus({
status: summary.status,
backgroundTasks: summary.backgroundTasks,
turnOutcome: summary.turnOutcome
})
)
)
}
/** Wire the host's own deps into a feed; keeps the host at one call site.
* `deps` is a thunk because the host builds the feed in a field initializer,
* before its constructor parameters are assigned. */
@@ -250,7 +204,7 @@ export class StructuredAgentSessionStatusFeed {
this.retireSettledChildrenOnNewTurn(sessionId, session, projection.rootTurnId)
const summary = this.summaryFor(sessionId, session, source, record, projection.state)
const previous = this.published.get(sessionId)
if (previous && summariesEqual(previous, summary)) {
if (previous && structuredStatusSummariesEqual(previous, summary)) {
if (!this.ownership.matchesLocation(sessionId, session.params.location)) {
this.sink(summary, session.params.location)
}
@@ -0,0 +1,57 @@
// Which status-summary changes reach every session list. The summary is broadcast to remote
// subscribers too, so only a change a session list shows may re-send it.
import { agentProviderSessionsEqual } from '../../../shared/agent-session-resume'
import {
agentSessionBackgroundTasksEqual,
type AgentSessionStatusSummary
} from '../../../shared/agent-session-wire'
import { isAgentStatusHeldOpenByChildWork } from '../../../shared/agent-lead-status-fold'
import { structuredAgentSessionAgentStatus } from '../../../shared/structured-agent-session-agent-status'
import { structuredStatusChildrenEqual } from './structured-agent-session-status-child-work'
export function structuredStatusSummariesEqual(
a: AgentSessionStatusSummary,
b: AgentSessionStatusSummary
): boolean {
return (
a.workspaceId === b.workspaceId &&
a.agent === b.agent &&
a.status === b.status &&
a.hostExecutionOwned === b.hostExecutionOwned &&
a.hostExecutionPhase === b.hostExecutionPhase &&
a.hostExecutionChild?.generation === b.hostExecutionChild?.generation &&
a.hostExecutionChild?.fence === b.hostExecutionChild?.fence &&
a.rewindBlockedReason === b.rewindBlockedReason &&
// A moved state clock changes ranking; row activity alone, including a subagent's, does not.
// An idle state the journal cannot date still republishes, since readers date it by `updatedAt`,
// and so does one live child work holds open: readers take each publish as its evidence.
a.statusStartedAt === b.statusStartedAt &&
(a.status !== 'idle' ||
a.updatedAt === b.updatedAt ||
(a.statusStartedAt !== undefined && !isIdleHeldOpenByChildWork(b))) &&
a.latestPrompt === b.latestPrompt &&
a.model === b.model &&
a.toolName === b.toolName &&
a.toolInput === b.toolInput &&
a.lastAssistantMessage === b.lastAssistantMessage &&
a.turnOutcome === b.turnOutcome &&
agentSessionBackgroundTasksEqual(a.backgroundTasks, b.backgroundTasks) &&
structuredStatusChildrenEqual(a.children, b.children) &&
agentProviderSessionsEqual(undefined, a.providerSession, b.providerSession)
)
}
/** Held open by the host's child records, read the way every row reads them. */
function isIdleHeldOpenByChildWork(summary: AgentSessionStatusSummary): boolean {
return (
summary.status === 'idle' &&
isAgentStatusHeldOpenByChildWork(
structuredAgentSessionAgentStatus({
status: summary.status,
childWork: summary.children ?? summary.backgroundTasks,
turnOutcome: summary.turnOutcome
})
)
)
}