diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx
index 188846461e4..1276287aca0 100644
--- a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx
+++ b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx
@@ -6,7 +6,6 @@ import type {
AgentSessionStatusEvent,
AgentSessionStatusSummary
} from '../../../../shared/agent-session-wire'
-import { buildSubagentChildRows } from '../sidebar/worktree-subagent-child-rows'
import { resolveAttention } from '../sidebar/smart-attention'
import { isExplicitAgentStatusFresh } from '@/lib/pane-agent-evidence'
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
@@ -220,116 +219,6 @@ describe('StructuredAgentSessionStatusBridge', () => {
expect(statuses()).toEqual([expect.objectContaining({ state: 'blocked' })])
})
- it('publishes agent-kind background tasks as the sidebar subagent children', async () => {
- render()
- await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
-
- act(() =>
- feed().emit({
- type: 'snapshot',
- sessions: [
- summary({
- backgroundTasks: [
- {
- id: 'child-1',
- kind: 'agent',
- name: 'deep_review',
- description: 'Review the diff',
- state: 'working',
- startedAt: 500
- },
- // A backgrounded shell is not a subagent; kinds stay distinct.
- { id: 'shell-1', kind: 'command', description: 'sleep 180', state: 'working' }
- ]
- })
- ]
- })
- )
- expect(statuses()).toEqual([
- expect.objectContaining({
- subagents: [
- {
- id: 'child-1',
- state: 'working',
- startedAt: 500,
- agentType: 'deep_review',
- description: 'Review the diff'
- }
- ]
- })
- ])
-
- // An unchanged roster must not rewrite the store.
- const writes = mocks.setAgentStatus.mock.calls.length
- act(() =>
- feed().emit({
- type: 'status',
- session: summary({
- backgroundTasks: [
- {
- id: 'child-1',
- kind: 'agent',
- name: 'deep_review',
- description: 'Review the diff',
- state: 'working',
- startedAt: 500
- },
- { id: 'shell-1', kind: 'command', description: 'sleep 180', state: 'working' }
- ]
- })
- })
- )
- expect(mocks.setAgentStatus.mock.calls.length).toBe(writes)
-
- act(() =>
- feed().emit({
- type: 'status',
- session: summary({
- updatedAt: 2,
- backgroundTasks: [
- { id: 'child-1', kind: 'agent', name: 'deep_review', state: 'waiting', startedAt: 500 }
- ]
- })
- })
- )
- expect(statuses()).toEqual([
- expect.objectContaining({
- subagents: [expect.objectContaining({ id: 'child-1', state: 'waiting' })]
- })
- ])
-
- // A summary without tasks ends the fan-out: children clear with it.
- act(() => feed().emit({ type: 'status', session: summary({ status: 'idle', updatedAt: 3 }) }))
- expect(statuses()).toEqual([expect.objectContaining({ subagents: undefined })])
- })
-
- it('keeps quiet live children authoritative and reconfirms them per session after reconnect', async () => {
- render()
- await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
- const live = summary({ backgroundTasks: [{ id: 'child', kind: 'agent', state: 'working' }] })
- const childState = () =>
- buildSubagentChildRows({
- parentEntry: statuses()[0],
- tab: structuredTab as never,
- parentIsFresh: false
- })[0]?.state
- act(() => feed().emit({ type: 'snapshot', sessions: [live] }))
- // A hook's evidence window has expired, but the host has not retracted its live task.
- expect(childState()).toBe('working')
- act(() => feed().emit({ type: 'end' }))
- expect(childState()).toBe('unverifiable')
- await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledTimes(2))
- act(() => feed(1).emit({ type: 'snapshot', sessions: [] }))
- expect(childState()).toBe('unverifiable')
- act(() => feed(1).emit({ type: 'status', session: live }))
- expect(childState()).toBe('working')
- const writes = mocks.setAgentStatus.mock.calls.length
- act(() => feed(1).emit({ type: 'status', session: live }))
- expect(mocks.setAgentStatus).toHaveBeenCalledTimes(writes)
- act(() => feed(1).emit({ type: 'status', session: summary({ backgroundTasks: [] }) }))
- expect(childState()).toBeUndefined()
- })
-
it('carries the model, the running tool line, and the last assistant message', async () => {
render()
await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx
index 1bb54dc904a..47d6aec460f 100644
--- a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx
+++ b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx
@@ -1,16 +1,7 @@
import { useEffect, useMemo, useSyncExternalStore } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { agentProviderSessionsEqual } from '../../../../shared/agent-session-resume'
-import type {
- AgentSessionBackgroundTask,
- AgentSessionStatusSummary
-} from '../../../../shared/agent-session-wire'
-import {
- AGENT_STATUS_MAX_SUBAGENTS,
- agentSubagentsEqual,
- type AgentSubagentSnapshot,
- type AgentSubagentState
-} from '../../../../shared/agent-status-types'
+import type { AgentSessionStatusSummary } from '../../../../shared/agent-session-wire'
import {
structuredAgentSessionPaneKey,
structuredAgentSessionStatusState
@@ -58,78 +49,17 @@ export function getStructuredAgentSessionTabs(
function useStructuredAgentSessionStatusSummary(
sessionId: string,
target: RuntimeClientTarget
-): { summary: AgentSessionStatusSummary | null; observation: 'live' | 'unverifiable' } {
+): AgentSessionStatusSummary | null {
const feed = useMemo(() => getStructuredAgentSessionStatusFeed(target), [target])
useEffect(() => feed.activate(), [feed])
- const summary = useSyncExternalStore(
+ return useSyncExternalStore(
feed.subscribe,
() => feed.getSnapshot().get(sessionId) ?? null,
() => null
)
- const observation = useSyncExternalStore(
- feed.subscribe,
- () => feed.getSessionObservation(sessionId),
- () => 'unverifiable' as const
- )
- return { summary, observation }
}
-/** Matches the wire-parse bound in `normalizeSubagentSnapshot`. */
-const SUBAGENT_ID_MAX_LENGTH = 64
-
-function subagentStateFromTask(task: AgentSessionBackgroundTask): AgentSubagentState {
- switch (task.state) {
- case 'waiting':
- return 'waiting'
- case 'blocked':
- return 'blocked'
- case 'done':
- case 'idle':
- case 'unverifiable':
- return 'idle'
- // Absent state is an old host's live task; live means working here.
- case 'working':
- case 'monitoring':
- case undefined:
- return 'working'
- }
-}
-
-/** Sidebar children for a structured session: the agent-kind background tasks
- * the host publishes, mapped to the sidebar's own subagent vocabulary rather
- * than widening it. Kinds stay distinct — a backgrounded shell never counts
- * as a subagent. */
-function subagentSnapshotsFromTasks(
- tasks: AgentSessionBackgroundTask[] | undefined
-): AgentSubagentSnapshot[] | undefined {
- if (!tasks) {
- return undefined
- }
- const snapshots: AgentSubagentSnapshot[] = []
- for (const task of tasks) {
- const id = task.id.trim()
- if (task.kind !== 'agent' || id.length === 0 || id.length > SUBAGENT_ID_MAX_LENGTH) {
- continue
- }
- snapshots.push({
- id,
- state: subagentStateFromTask(task),
- startedAt: task.startedAt ?? 0,
- ...(task.name ? { agentType: task.name } : {}),
- ...(task.description ? { description: task.description } : {})
- })
- if (snapshots.length >= AGENT_STATUS_MAX_SUBAGENTS) {
- break
- }
- }
- return snapshots.length > 0 ? snapshots : undefined
-}
-
-function projectStatus(
- tab: StructuredTab,
- summary: AgentSessionStatusSummary | null,
- observation: 'live' | 'unverifiable'
-): void {
+function projectStatus(tab: StructuredTab, summary: AgentSessionStatusSummary | null): void {
const paneKey = structuredAgentSessionPaneKey(tab.id, tab.entityId)
const store = useAppStore.getState()
// No persisted turn yet (or nothing known): the row shows no agent status at all.
@@ -139,7 +69,6 @@ function projectStatus(
}
return
}
- const subagents = subagentSnapshotsFromTasks(summary.backgroundTasks)
const desired = {
// Shared with `worktree ps`, so the CLI and this row cannot disagree about one session.
state: structuredAgentSessionStatusState(summary.status),
@@ -151,7 +80,6 @@ function projectStatus(
...(summary.toolName ? { toolName: summary.toolName } : {}),
...(summary.toolInput ? { toolInput: summary.toolInput } : {}),
...(summary.lastAssistantMessage ? { lastAssistantMessage: summary.lastAssistantMessage } : {}),
- ...(subagents ? { subagents, subagentObservation: observation } : {}),
sessionBoundary: false
} as const
const current = store.agentStatusByPaneKey?.[paneKey]
@@ -164,8 +92,6 @@ function projectStatus(
current.toolName === summary.toolName &&
current.toolInput === summary.toolInput &&
current.lastAssistantMessage === summary.lastAssistantMessage &&
- agentSubagentsEqual(current.subagents, subagents) &&
- current.subagentObservation === desired.subagentObservation &&
current.sessionBoundary === desired.sessionBoundary &&
current.updatedAt === summary.updatedAt &&
current.terminalTitle === tab.label &&
@@ -212,10 +138,10 @@ function StructuredAgentSessionStatusProjection({ tab }: { tab: StructuredTab })
() => getActiveRuntimeTarget({ activeRuntimeEnvironmentId: environmentId }),
[environmentId]
)
- const { summary, observation } = useStructuredAgentSessionStatusSummary(tab.entityId, target)
+ const summary = useStructuredAgentSessionStatusSummary(tab.entityId, target)
useEffect(() => {
- projectStatus(tab, summary, observation)
- }, [summary, observation, tab])
+ projectStatus(tab, summary)
+ }, [summary, tab])
useEffect(
() => () =>
useAppStore.getState().removeAgentStatus(structuredAgentSessionPaneKey(tab.id, tab.entityId)),
diff --git a/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts b/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts
index 406a72f7328..5135eea6933 100644
--- a/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts
+++ b/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts
@@ -29,13 +29,8 @@ export function buildSubagentChildRows(args: {
return []
}
return subagents.map((subagent) => {
- const observation = args.parentEntry.subagentObservation
- const fresh = observation === 'live' || (observation === undefined && args.parentIsFresh)
- const activeState = fresh && subagent.state !== 'idle' ? subagent.state : undefined
- const state =
- observation === 'unverifiable' && subagent.state !== 'idle'
- ? 'unverifiable'
- : (activeState ?? 'idle')
+ const activeState = args.parentIsFresh && subagent.state !== 'idle' ? subagent.state : undefined
+ const state = activeState ?? 'idle'
const startedAt = subagent.startedAt > 0 ? subagent.startedAt : args.parentEntry.stateStartedAt
const paneKey = subagentRowKey(args.parentEntry.paneKey, subagent.id)
const entry: AgentStatusEntry = {
diff --git a/src/renderer/src/runtime/structured-agent-session-status-feed.test.ts b/src/renderer/src/runtime/structured-agent-session-status-feed.test.ts
index de1617cdb1b..d9cb4dd3d72 100644
--- a/src/renderer/src/runtime/structured-agent-session-status-feed.test.ts
+++ b/src/renderer/src/runtime/structured-agent-session-status-feed.test.ts
@@ -125,58 +125,6 @@ describe('structured agent session status feed', () => {
expect(feed.getSnapshot().get('session-2')?.status).toBe('idle')
})
- it('confirms sessions per connection and rejects late events from a disconnected stream', async () => {
- const feed = getStructuredAgentSessionStatusFeed(LOCAL)
- const deactivate = feed.activate()
- await vi.advanceTimersByTimeAsync(0)
- hostEmit()({ type: 'snapshot', sessions: [summary('one'), summary('two')] })
- expect(feed.getSessionObservation('one')).toBe('live')
- await vi.advanceTimersByTimeAsync(31 * 60_000)
- expect(feed.getSessionObservation('one')).toBe('live')
- expect(mocks.subscribeStatus).toHaveBeenCalledOnce()
- hostEmit()({ type: 'end' })
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- hostEmit()({ type: 'status', session: summary('one') })
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- await vi.advanceTimersByTimeAsync(300)
- hostEmit(1)({ type: 'snapshot', sessions: [summary('two')] })
- expect(feed.getSessionObservation('two')).toBe('live')
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- expect(feed.getSnapshot().has('one')).toBe(true)
- hostEmit(1)({ type: 'status', session: summary('one') })
- expect(feed.getSessionObservation('one')).toBe('live')
- deactivate()
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- expect(vi.getTimerCount()).toBe(0)
- })
-
- it.each(['error', 'close', 'reject'] as const)(
- 'revokes confirmation on subscription %s',
- async (failure) => {
- let reject!: (error: Error) => void
- mocks.subscribeStatus.mockImplementationOnce(
- () =>
- new Promise((_resolve, rejectPromise) => {
- reject = rejectPromise
- })
- )
- const feed = getStructuredAgentSessionStatusFeed(LOCAL)
- feed.activate()
- hostEmit()({ type: 'status', session: summary('one') })
- expect(feed.getSessionObservation('one')).toBe('live')
- if (failure === 'reject') {
- reject(new Error('disconnected'))
- } else {
- mocks.subscribeStatus.mock.calls[0][failure === 'error' ? 2 : 3]()
- }
- await vi.advanceTimersByTimeAsync(0)
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- hostEmit()({ type: 'status', session: summary('one') })
- expect(feed.getSessionObservation('one')).toBe('unverifiable')
- expect(vi.getTimerCount()).toBe(1)
- }
- )
-
it('stops a pending reconnect when the feeds are reset between tests', async () => {
getStructuredAgentSessionStatusFeed(LOCAL).activate()
await vi.advanceTimersByTimeAsync(0)
diff --git a/src/renderer/src/runtime/structured-agent-session-status-feed.ts b/src/renderer/src/runtime/structured-agent-session-status-feed.ts
index 4a50717f638..8308ac8729a 100644
--- a/src/renderer/src/runtime/structured-agent-session-status-feed.ts
+++ b/src/renderer/src/runtime/structured-agent-session-status-feed.ts
@@ -21,7 +21,6 @@ export type StructuredAgentSessionStatusSnapshot = ReadonlyMap () => void
getSnapshot: () => StructuredAgentSessionStatusSnapshot
- getSessionObservation: (sessionId: string) => 'live' | 'unverifiable'
subscribe: (listener: () => void) => () => void
}
@@ -38,7 +37,6 @@ export function structuredAgentSessionStatusFeedKey(target: RuntimeClientTarget)
function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
let snapshot: StructuredAgentSessionStatusSnapshot = new Map()
- const confirmedSessions = new Set()
const listeners = new Set<() => void>()
const activations = new Set()
let generation = 0
@@ -62,14 +60,12 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
// the first snapshot can be empty and dropping those rows flickers every one to no-status.
const next = new Map(snapshot)
for (const session of event.sessions) {
- confirmedSessions.add(session.sessionId)
next.set(session.sessionId, session)
}
setSnapshot(next)
return
}
if (event.type === 'status') {
- confirmedSessions.add(event.session.sessionId)
const next = new Map(snapshot)
next.set(event.session.sessionId, event.session)
setSnapshot(next)
@@ -103,6 +99,15 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
emit()
}
}
+ const fenceCandidateAndReconnect = (candidate: number): void => {
+ if (candidate !== generation) {
+ return
+ }
+ generation += 1
+ revokeSnapshotOwnership()
+ dropHandle()
+ scheduleReconnect(generation)
+ }
let open = (): void => {}
const scheduleReconnect = (candidate: number): void => {
if (!active(candidate) || reconnectTimer) {
@@ -117,19 +122,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
}
}, delay)
}
- // Losing contact is never exit: the sessions go unverifiable and this client stops
- // claiming host-owned execution, but nothing here settles them.
- const loseConnection = (candidate: number): void => {
- if (candidate !== generation) {
- return
- }
- generation += 1
- confirmedSessions.clear()
- revokeSnapshotOwnership()
- emit()
- dropHandle()
- scheduleReconnect(generation)
- }
const subscribeToHost = (candidate: number): void => {
void subscribeStructuredAgentSessionStatus(
target,
@@ -138,19 +130,19 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
return
}
if (event.type === 'end') {
- loseConnection(candidate)
+ fenceCandidateAndReconnect(candidate)
return
}
applyEvent(event)
},
() => {
if (active(candidate)) {
- loseConnection(candidate)
+ fenceCandidateAndReconnect(candidate)
}
},
() => {
if (active(candidate)) {
- loseConnection(candidate)
+ fenceCandidateAndReconnect(candidate)
}
}
)
@@ -161,7 +153,13 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
opened.unsubscribe()
}
})
- .catch(() => loseConnection(candidate))
+ .catch(() => {
+ if (active(candidate)) {
+ fenceCandidateAndReconnect(candidate)
+ } else {
+ scheduleReconnect(candidate)
+ }
+ })
}
open = (): void => {
const candidate = ++generation
@@ -188,7 +186,7 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
}
console.warn('[structured-session-status] host too old for the status feed', environmentId)
})
- .catch(() => loseConnection(candidate))
+ .catch(() => scheduleReconnect(candidate))
}
const stop = (): void => {
generation += 1
@@ -196,9 +194,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
dropHandle()
revokeSnapshotOwnership()
reconnectAttempt = 0
- // Teardown only runs once nothing is activated, so re-confirmation is the next
- // subscribe's job and there is no mounted reader left to notify.
- confirmedSessions.clear()
}
return {
@@ -216,8 +211,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
}
},
getSnapshot: () => snapshot,
- getSessionObservation: (sessionId) =>
- confirmedSessions.has(sessionId) ? 'live' : 'unverifiable',
subscribe: (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
diff --git a/src/renderer/src/store/slices/agent-status-contract.ts b/src/renderer/src/store/slices/agent-status-contract.ts
index eaab5bb78ae..453fef02139 100644
--- a/src/renderer/src/store/slices/agent-status-contract.ts
+++ b/src/renderer/src/store/slices/agent-status-contract.ts
@@ -83,7 +83,6 @@ export type AgentLaunchConfigRegistryEntry = {
}
export type AgentStatusPayload = ParsedAgentStatusPayload & {
- subagentObservation?: AgentStatusEntry['subagentObservation']
orchestration?: AgentStatusOrchestrationContext
promptInteractionKey?: string
restoredUnconfirmed?: boolean
diff --git a/src/renderer/src/store/slices/agent-status-live-entry-builder.ts b/src/renderer/src/store/slices/agent-status-live-entry-builder.ts
index b161e51bc04..63ba0a5afc9 100644
--- a/src/renderer/src/store/slices/agent-status-live-entry-builder.ts
+++ b/src/renderer/src/store/slices/agent-status-live-entry-builder.ts
@@ -256,7 +256,6 @@ export function buildAgentStatusLiveEntry(
lastAssistantMessageIsToolOutput: payload.lastAssistantMessageIsToolOutput,
...(lastCompletedAssistantMessage ? { lastCompletedAssistantMessage } : {}),
orchestration,
- ...(payload.subagentObservation ? { subagentObservation: payload.subagentObservation } : {}),
subagents: agentSubagentsEqual(existing?.subagents, payload.subagents)
? existing?.subagents
: payload.subagents,
diff --git a/src/shared/agent-status-types.ts b/src/shared/agent-status-types.ts
index 3fc6c6b7f34..cc0ad487565 100644
--- a/src/shared/agent-status-types.ts
+++ b/src/shared/agent-status-types.ts
@@ -102,8 +102,6 @@ export type AgentSubagentSnapshot = {
}
export type AgentStatusEntry = {
- /** Renderer-local status-feed confirmation for children; absent on hook rows. */
- subagentObservation?: 'live' | 'unverifiable'
state: AgentStatusState
/** Ongoing work that does not require foreground agent execution. Only valid while working. */
workingMode?: AgentWorkingMode