Files
orca/src/shared/agent-session-start-time.ts
T
Brennan Benson 004907eb11 fix(sidebar): keep agent rows ordered by their session start once state history wraps (#14654)
* fix(sidebar): keep agent rows ordered by their session start once history wraps

Sidebar agent rows are ordered by the session's first reported state, read as
`stateHistory[0].startedAt`. `stateHistory` is a rolling window capped at
AGENT_STATE_HISTORY_MAX that trims oldest-first, so past ~20 transitions that
row is a recent turn: the row's identity clock silently becomes an activity
clock and busy agents re-sort themselves on every turn.

Latch the session origin in `firstStateStartedAt` when the entry is built and
read it through `agentEntrySessionStartedAt`, keeping the history lookup as a
fallback for entries that predate the field.

* test(sidebar): cover row order through activity and retention

* fix(sidebar): preserve session row order through activity and retention

* test: complete paired terminal status fixtures

* fix: carry accepted session origins across paired rows

* test: cover paired origin work and mobile action parity

* test(mobile): expect sessionLive in the phone chat identity after main's start-a-chat change
2026-10-29 14:27:16 -07:00

78 lines
2.6 KiB
TypeScript

import type { AgentStatusEntry } from './agent-status-types'
import { agentProviderSessionsEqual } from './agent-session-resume'
import { sameAgentStatusTerminalOwner } from './agent-status-terminal-owner'
type SessionIdentity = Pick<
AgentStatusEntry,
'agentType' | 'providerSession' | 'connectionId' | 'worktreeId'
>
function finite(value: number | undefined): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}
export function agentEntrySessionStartedAt(
entry: Pick<AgentStatusEntry, 'firstStateStartedAt' | 'stateHistory' | 'stateStartedAt'>
): number {
return (
finite(entry.firstStateStartedAt) ??
finite(entry.stateHistory[0]?.startedAt) ??
entry.stateStartedAt
)
}
/** Missing identity enriches a row; only accepted contradictory identity starts another session. */
export function resolveAgentSessionStartedAt(args: {
entry: AgentStatusEntry
previous?: AgentStatusEntry
previousStartedAt?: number
identity?: SessionIdentity
}): number {
const { entry, previous, previousStartedAt, identity = entry } = args
if (!previous) {
return agentEntrySessionStartedAt(entry)
}
const agentChanged =
previous.agentType &&
previous.agentType !== 'unknown' &&
identity.agentType &&
identity.agentType !== 'unknown' &&
previous.agentType !== identity.agentType
const sessionChanged =
previous.providerSession &&
identity.providerSession &&
!agentProviderSessionsEqual(
identity.agentType,
{
...previous.providerSession,
transcriptPath:
previous.providerSession.transcriptPath ?? identity.providerSession.transcriptPath
},
{
...identity.providerSession,
transcriptPath:
identity.providerSession.transcriptPath ?? previous.providerSession.transcriptPath
}
)
const worktreeChanged =
previous.worktreeId &&
identity.worktreeId &&
!sameAgentStatusTerminalOwner(
{ worktreeId: previous.worktreeId, connectionId: null },
{ worktreeId: identity.worktreeId, connectionId: null }
)
const connectionChanged =
previous.connectionId !== undefined &&
identity.connectionId !== undefined &&
!sameAgentStatusTerminalOwner(previous, identity)
if (agentChanged || sessionChanged || worktreeChanged || connectionChanged) {
// State clocks can span sessions; only the accepted source dates a replacement.
return finite(entry.firstStateStartedAt) ?? finite(entry.evidenceObservedAt) ?? entry.updatedAt
}
return (
finite(previous.firstStateStartedAt) ??
finite(previousStartedAt) ??
agentEntrySessionStartedAt(previous)
)
}