mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
feat(native-chat): the sidebar lists running subagents; the strip, running then the newest finished
The host keeps every child record; what each surface lists is picked from them on every read, so nothing is stored twice. The status summary, which every session list reads, now carries only running children (and a finished one whose shell still runs, which reads monitoring): a finished or failed subagent leaves the sidebar and stays in the chat's strip. The strip lists every running child, then the newest finished ones, 100 rows in all; more than 100 running all show. This matches common practice: sidebars show live subagents, and finished ones stay in the chat's panel, newest first. No wire field is added. An older client reads fewer rows: its legacy task lists were already live-only in the summary, and the strip's settled tasks come from the same bounded roster.
This commit is contained in:
+7
-4
@@ -5,6 +5,7 @@ import type {
|
||||
} from '../../../shared/agent-session-wire'
|
||||
import type { AgentChildWorkView } from '../../../shared/agent-status-child-work-view'
|
||||
import { structuredChildWorkLegacyTasks } from '../../../shared/structured-agent-session-child-work-legacy'
|
||||
import { structuredStripChildWork } from '../../../shared/structured-agent-session-child-work-selection'
|
||||
import { readStructuredAgentSessionHistoryResult } from './structured-agent-session-history-result'
|
||||
import type {
|
||||
AgentSessionSubscribers,
|
||||
@@ -16,8 +17,9 @@ import type {
|
||||
} from './structured-agent-session-host-types'
|
||||
import { structuredAgentSessionConversationFence } from './structured-agent-session-provider-child'
|
||||
|
||||
/** The chat strip's roster: the host's child records for one session, as the same views the status
|
||||
* summary carries, with the legacy task rows an older client reads derived from them. */
|
||||
/** The chat strip's roster: the host's child records for one session (every running child, then
|
||||
* the newest finished ones, up to the strip's row budget), with the legacy task rows an older
|
||||
* client reads derived from them. */
|
||||
export class StructuredAgentSessionBackgroundTaskChannel {
|
||||
private readonly published = new Map<string, string>()
|
||||
|
||||
@@ -90,10 +92,11 @@ export class StructuredAgentSessionBackgroundTaskChannel {
|
||||
|
||||
private state(sessionId: string): AgentSessionBackgroundTaskState | null | undefined {
|
||||
const session = this.sessions.get(sessionId)
|
||||
const views = session ? this.readChildWork(sessionId) : undefined
|
||||
if (!session || views === undefined) {
|
||||
const stored = session ? this.readChildWork(sessionId) : undefined
|
||||
if (!session || stored === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const views = structuredStripChildWork(stored)
|
||||
const stops = this.deps.adapter.backgroundTaskStops?.(sessionId)
|
||||
if (views.length === 0) {
|
||||
// As before: a session no live provider holds says nothing, a live one says "none".
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// What each surface lists of a session's child records: the sidebar its running children, the
|
||||
// chat's strip every running child and then the newest finished ones, up to 100 rows.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AgentChildWorkView } from '../../../shared/agent-status-child-work-view'
|
||||
import {
|
||||
STRUCTURED_STRIP_CHILD_WORK_LIMIT as LIMIT,
|
||||
structuredSidebarChildWork,
|
||||
structuredStripChildWork
|
||||
} from '../../../shared/structured-agent-session-child-work-selection'
|
||||
import {
|
||||
attach,
|
||||
hostTestState,
|
||||
serveHostTestChildWork
|
||||
} from './structured-agent-session-host-test-harness'
|
||||
import { HOST_TEST_SESSION as SESSION } from './structured-agent-session-host-test-data'
|
||||
|
||||
function running(id: string, over: Partial<AgentChildWorkView> = {}): AgentChildWorkView {
|
||||
return {
|
||||
id,
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
firstObservedAt: 1,
|
||||
observedAt: 1,
|
||||
stoppable: false,
|
||||
invocation: { invocationId: `spawn-${id}`, generation: 1 },
|
||||
...over
|
||||
}
|
||||
}
|
||||
|
||||
function finished(id: string, settledAt: number): AgentChildWorkView {
|
||||
return running(id, {
|
||||
state: 'done',
|
||||
membership: 'settled',
|
||||
outcome: 'succeeded',
|
||||
observedAt: settledAt,
|
||||
settledAt
|
||||
})
|
||||
}
|
||||
|
||||
const ids = (views: readonly AgentChildWorkView[]) => views.map((view) => view.id)
|
||||
|
||||
describe('the sidebar lists running children only', () => {
|
||||
it('drops finished and failed children, and keeps a finished one whose shell still runs', () => {
|
||||
const failed = { ...finished('failed', 5), outcome: 'failed' as const }
|
||||
const views = [
|
||||
running('working'),
|
||||
finished('done', 4),
|
||||
failed,
|
||||
finished('owner', 3),
|
||||
running('shell', { kind: 'command', parentChildWorkId: 'owner' })
|
||||
]
|
||||
expect(ids(structuredSidebarChildWork(views))).toEqual(['working', 'owner', 'shell'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('the strip lists running children, then the newest finished, up to 100 rows', () => {
|
||||
it('lists every child while they fit', () => {
|
||||
const views = [running('a'), finished('b', 2), finished('c', 3)]
|
||||
expect(ids(structuredStripChildWork(views))).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('keeps every running child and the newest finished ones, in the order the store holds them', () => {
|
||||
const finishedViews = Array.from({ length: 150 }, (_, index) =>
|
||||
finished(`done-${index}`, 1_000 + index)
|
||||
)
|
||||
const runningViews = Array.from({ length: 10 }, (_, index) => running(`run-${index}`))
|
||||
const listed = structuredStripChildWork([...finishedViews, ...runningViews])
|
||||
expect(listed).toHaveLength(LIMIT)
|
||||
// The 90 newest finished children (settled latest), then the running ones, as the store orders.
|
||||
expect(ids(listed)).toEqual([
|
||||
...Array.from({ length: 90 }, (_, index) => `done-${index + 60}`),
|
||||
...ids(runningViews)
|
||||
])
|
||||
})
|
||||
|
||||
it('shows every running child when more than 100 run, and no finished one', () => {
|
||||
const runningViews = Array.from({ length: 120 }, (_, index) => running(`run-${index}`))
|
||||
const listed = structuredStripChildWork([finished('done', 5), ...runningViews])
|
||||
expect(ids(listed)).toEqual(ids(runningViews))
|
||||
})
|
||||
})
|
||||
|
||||
describe("the host's strip channel and summary list what these pick", () => {
|
||||
it('sends the strip the bounded roster and the sidebar the running children', async () => {
|
||||
const records = [
|
||||
...Array.from({ length: 150 }, (_, index) => finished(`done-${index}`, 1_000 + index)),
|
||||
running('run')
|
||||
]
|
||||
serveHostTestChildWork(() => records)
|
||||
await attach()
|
||||
const { host } = hostTestState()
|
||||
const page = await host.history({ sessionId: SESSION, direction: 'tail' })
|
||||
const children = page.ok ? (page.page.backgroundTasks?.children ?? []) : []
|
||||
expect(children).toHaveLength(LIMIT)
|
||||
expect(children.at(-1)?.id).toBe('run')
|
||||
expect(children[0]?.id).toBe('done-51')
|
||||
const summaries: string[][] = []
|
||||
host.subscribeStatus({
|
||||
id: 'list',
|
||||
emit: (event) => {
|
||||
if (event.type === 'snapshot') {
|
||||
summaries.push(event.sessions.flatMap((session) => ids(session.children ?? [])))
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(summaries.at(-1)).toEqual(['run'])
|
||||
})
|
||||
})
|
||||
+7
-4
@@ -14,6 +14,7 @@ import type { AgentSessionBackgroundTask } from '../../../shared/agent-session-w
|
||||
import type { AgentChildWorkView } from '../../../shared/agent-status-child-work-view'
|
||||
import { agentChildWorkViewsEqual } from '../../../shared/agent-status-child-work-view-wire'
|
||||
import { structuredChildWorkLegacyTasks } from '../../../shared/structured-agent-session-child-work-legacy'
|
||||
import { structuredSidebarChildWork } from '../../../shared/structured-agent-session-child-work-selection'
|
||||
|
||||
/** An evidence clock that advanced by less than this does not re-broadcast a summary.
|
||||
* Invariant: every reader of the summary's child clocks shows staleness no finer than this (today,
|
||||
@@ -27,16 +28,18 @@ export type StructuredStatusChildWork = {
|
||||
backgroundTasks?: AgentSessionBackgroundTask[]
|
||||
}
|
||||
|
||||
/** The summary's child fields: the views without usage, and the live legacy tasks an older client
|
||||
* reads, derived from the same views so the two cannot disagree. */
|
||||
/** The summary's child fields: the running views without usage, since the sidebar lists running
|
||||
* children only, and the live legacy tasks an older client reads, derived from the same views so
|
||||
* the two cannot disagree. */
|
||||
export function structuredStatusChildWork(
|
||||
views: readonly AgentChildWorkView[] | undefined,
|
||||
provider: AgentSessionHandleProvider
|
||||
): StructuredStatusChildWork {
|
||||
if (!views || views.length === 0) {
|
||||
const running = views ? structuredSidebarChildWork(views) : []
|
||||
if (running.length === 0) {
|
||||
return {}
|
||||
}
|
||||
const children = views.map(({ totalTokens: _totalTokens, ...view }) => view)
|
||||
const children = running.map(({ totalTokens: _totalTokens, ...view }) => view)
|
||||
// `tasks` only, the live rows: an old client folds every listed task into the parent, and
|
||||
// reads a failed one's legacy `blocked` as still running. Dies with the legacy shapes; see the
|
||||
// death condition in `structured-agent-session-child-work-legacy`.
|
||||
|
||||
+22
-5
@@ -186,7 +186,8 @@ describe('structured status summary child records', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('lists a finished child on the summary, never in the task list an older client folds', async () => {
|
||||
// The sidebar lists running children only; a finished one stays in the chat's strip.
|
||||
it('lists only running children on the summary, and no finished one in any of its lists', async () => {
|
||||
const { feed, events, views } = await feedWithChildren()
|
||||
const failed = childView({
|
||||
id: 'child-2',
|
||||
@@ -203,8 +204,26 @@ describe('structured status summary child records', () => {
|
||||
// An older client reads a failed task's legacy `blocked` as still running.
|
||||
expect(lastSummary(events)).toMatchObject({
|
||||
backgroundTasks: [LEGACY_TASK],
|
||||
children: [childView(), failed]
|
||||
children: [childView()]
|
||||
})
|
||||
views.current = () => [failed]
|
||||
feed.publish(SESSION)
|
||||
expect(lastSummary(events)).not.toHaveProperty('children')
|
||||
expect(lastSummary(events)).not.toHaveProperty('backgroundTasks')
|
||||
})
|
||||
|
||||
it('keeps a finished child on the summary while a shell it launched still runs', async () => {
|
||||
const { feed, events, views } = await feedWithChildren()
|
||||
const owner = childView({ state: 'done', membership: 'settled', outcome: 'succeeded' })
|
||||
const shell = childView({
|
||||
id: 'child-2',
|
||||
providerId: 'task-2',
|
||||
kind: 'command',
|
||||
parentChildWorkId: 'child-1'
|
||||
})
|
||||
views.current = () => [owner, shell]
|
||||
feed.publish(SESSION)
|
||||
expect(lastSummary(events)?.children).toEqual([owner, shell])
|
||||
})
|
||||
|
||||
it("keeps a Claude child's own id, and a Codex subagent's the id its row always carried", async () => {
|
||||
@@ -285,9 +304,7 @@ describe('structured status summary child records', () => {
|
||||
|
||||
it('stops listing children once the row leaves the sink, even with no close', async () => {
|
||||
const { feed, events, views } = await feedWithChildren()
|
||||
views.current = () => [
|
||||
childView({ membership: 'settled', state: 'done', outcome: 'succeeded' })
|
||||
]
|
||||
views.current = () => [childView()]
|
||||
feed.publish(SESSION)
|
||||
expect(lastSummary(events)?.children).toHaveLength(1)
|
||||
feed.forget(SESSION)
|
||||
|
||||
@@ -218,12 +218,13 @@ export class StructuredAgentSessionStatusFeed {
|
||||
}
|
||||
}
|
||||
|
||||
/** A finished child stays listed, with its outcome, until the session's own next turn begins,
|
||||
/** A finished child's record stays, with its outcome, until the session's own next turn begins,
|
||||
* unless it still owns live work. The provider ending the session removes nothing: children
|
||||
* still live settle `unknown`, and settled ones stay. The one earlier death is the host letting
|
||||
* go of the session, whose row takes every child record with it (see `forget`). Nothing caps how
|
||||
* many settle within a turn, and a command never settles: its record goes when it stops. The
|
||||
* summary and the strip both re-read the store, so each step removes a child from both. */
|
||||
* many records a turn keeps, and a command never settles: its record goes when it stops. What
|
||||
* each surface lists of them is picked on every read (`structured-agent-session-child-work-
|
||||
* selection`): the sidebar only running children, the strip running then newest finished. */
|
||||
private retireSettledChildrenOnNewTurn(
|
||||
sessionId: string,
|
||||
session: StatusFeedSession,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// The switch, end to end on a host with no renderer: a Codex session's frames reach the host's
|
||||
// canonical store, and the status summary every session list reads and the chat strip's channel
|
||||
// both carry the same child records from it, while the parent row is folded from them too.
|
||||
// canonical store; the status summary every session list reads carries the running children, the
|
||||
// chat strip's channel carries those and the finished ones, and the parent row is folded from the
|
||||
// same records.
|
||||
//
|
||||
// Deliberately NOT a comparison against the provider tracker's task list: that tracker and the
|
||||
// record producer read the same child executions, so agreeing with it could not catch a defect in
|
||||
@@ -91,7 +92,7 @@ describe('the chat strip and the session list read the same host child records',
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('carries one record per child to both, and folds the parent row from them', async () => {
|
||||
it('carries running children to both and finished ones to the strip, and folds the parent row from them', async () => {
|
||||
const connections: CodexAppServerConnectionHandlers[] = []
|
||||
const openConnection: typeof openCodexAppServerConnection = async (_launch, handlers = {}) => {
|
||||
connections.push(handlers)
|
||||
@@ -293,9 +294,10 @@ describe('the chat strip and the session list read the same host child records',
|
||||
exitCode: 0
|
||||
})
|
||||
expect(parentRow()).toMatchObject({ state: 'done' })
|
||||
// The finished child stays listed, with how it ended, until the session's next turn; the dev
|
||||
// server's record went when its process exited.
|
||||
both([
|
||||
// The sidebar lists running children only; the finished child stays in the strip, with how it
|
||||
// ended, until the session's next turn. The dev server's record went when its process exited.
|
||||
expect(summaries.at(-1)).not.toHaveProperty('children')
|
||||
expect(stripRows()).toEqual([
|
||||
{
|
||||
kind: 'agent',
|
||||
description: 'review',
|
||||
@@ -310,12 +312,13 @@ describe('the chat strip and the session list read the same host child records',
|
||||
expect(strip.at(-1)).toBeNull()
|
||||
|
||||
// The reviewer's next run is still going when the provider exits: it settles with an outcome
|
||||
// nobody reported, and both keep listing it.
|
||||
// nobody reported, and leaves the sidebar while the strip keeps listing it.
|
||||
await notify('turn/started', { threadId: REVIEWER, turn: { id: 'r2', status: 'inProgress' } })
|
||||
both([{ kind: 'agent', description: 'review', state: 'working', membership: 'live' }])
|
||||
connections[0]?.onExit?.(new Error('scripted provider exit'))
|
||||
await settle()
|
||||
both([
|
||||
expect(summaries.at(-1)).not.toHaveProperty('children')
|
||||
expect(stripRows()).toEqual([
|
||||
{
|
||||
kind: 'agent',
|
||||
description: 'review',
|
||||
@@ -325,7 +328,8 @@ describe('the chat strip and the session list read the same host child records',
|
||||
}
|
||||
])
|
||||
|
||||
// Closing its tab lets go of the session: its row, and every child record with it, leave both.
|
||||
// Closing its tab lets go of the session: its row, and every child record with it, leave the
|
||||
// store.
|
||||
const subject = parentSubject(summaries)
|
||||
await host.setSessionTabVisibility(SESSION, false)
|
||||
await host.close(SESSION)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { AgentSessionBackgroundTask } from '../../../shared/agent-session-wire'
|
||||
import type { AgentChildWorkView } from '../../../shared/agent-status-child-work-view'
|
||||
import type { AgentStatusEntry } from '../../../shared/agent-status-types'
|
||||
import { structuredSidebarChildWork } from '../../../shared/structured-agent-session-child-work-selection'
|
||||
import type { TerminalTab } from '../../../shared/terminal-tab-types'
|
||||
import DashboardAgentRow from '@/components/dashboard/DashboardAgentRow'
|
||||
import { NativeChatBackgroundTasksStatus } from '@/components/native-chat/NativeChatBackgroundTasksStatus'
|
||||
@@ -283,13 +284,20 @@ const FULL_ROW_DETAIL: Record<string, { shows: string[]; hides?: string[] }> = {
|
||||
'parked, still live': { shows: [] }
|
||||
}
|
||||
|
||||
// The sidebar lists what the host's summary carries: running children only. A finished child is
|
||||
// read on the strip, and leaves the sidebar.
|
||||
describe('a child reads the same in the sidebar and the chat strip', () => {
|
||||
it.each(SCENARIOS)('%s', (name, children, expected) => {
|
||||
const [sidebar] = sidebarRows(parentWith(children))
|
||||
const listed = structuredSidebarChildWork(children)
|
||||
const [strip] = stripRows(children)
|
||||
expect(sidebar).toEqual(expected)
|
||||
expect(strip).toEqual(expected)
|
||||
const full = fullRow(parentWith(children))
|
||||
if (listed.length === 0) {
|
||||
expect(sidebarRows(parentWith(listed))).toEqual([])
|
||||
return
|
||||
}
|
||||
const [sidebar] = sidebarRows(parentWith(listed))
|
||||
expect(sidebar).toEqual(expected)
|
||||
const full = fullRow(parentWith(listed))
|
||||
expect(full.labels).toContain(expected.dot)
|
||||
expect(full.text).toContain(expected.lead === expected.dot ? expected.trail : expected.lead)
|
||||
for (const text of FULL_ROW_DETAIL[name].shows) {
|
||||
@@ -300,15 +308,22 @@ describe('a child reads the same in the sidebar and the chat strip', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('names an unlabeled child by the same state on every surface', () => {
|
||||
it('names an unlabeled finished child by its state on the strip, and leaves it off the sidebar', () => {
|
||||
const children = [
|
||||
settled('failed', { description: undefined, agentType: undefined, lastMessage: 'Exit 2' })
|
||||
]
|
||||
const [sidebar] = sidebarRows(parentWith(children))
|
||||
const [strip] = stripRows(children)
|
||||
expect(sidebar.lead).toBe('Failed')
|
||||
expect(strip.lead).toBe('Failed')
|
||||
expect(fullRow(parentWith(children)).text).toContain('Failed')
|
||||
expect(sidebarRows(parentWith(structuredSidebarChildWork(children)))).toEqual([])
|
||||
})
|
||||
|
||||
it('names an unlabeled running child by the same state on every surface', () => {
|
||||
const children = [view('child', { description: undefined, agentType: undefined })]
|
||||
const [sidebar] = sidebarRows(parentWith(structuredSidebarChildWork(children)))
|
||||
const [strip] = stripRows(children)
|
||||
expect(sidebar.lead).toBe('Working')
|
||||
expect(strip.lead).toBe('Working')
|
||||
expect(fullRow(parentWith(children)).text).toContain('Working')
|
||||
})
|
||||
|
||||
it('shows the monitoring icon on the full sidebar row too', () => {
|
||||
|
||||
@@ -50,28 +50,28 @@ function renderCompactAgentRow(props: React.ComponentProps<typeof CompactAgentRo
|
||||
}
|
||||
|
||||
describe('worktree card agent summary', () => {
|
||||
it('keeps a failed child in the groups and the counts', () => {
|
||||
const failed: DashboardAgentRowData = {
|
||||
// The sidebar lists running children only, so a child row it counts is one that runs.
|
||||
it("counts a running child by its own row's state", () => {
|
||||
const running: DashboardAgentRowData = {
|
||||
...monitoringAgent(),
|
||||
paneKey: 'tab-1:leaf-1\u0000subagent:child',
|
||||
rowSource: 'subagent',
|
||||
state: 'blocked',
|
||||
state: 'working',
|
||||
childRow: {
|
||||
id: 'child',
|
||||
kind: 'agent',
|
||||
displayState: 'failed',
|
||||
displayState: 'waiting',
|
||||
name: 'Fuzz the tokenizer',
|
||||
detail: null,
|
||||
firstObservedAt: 1,
|
||||
recencyAt: 1,
|
||||
canStop: false,
|
||||
settled: true,
|
||||
settled: false,
|
||||
owned: []
|
||||
}
|
||||
}
|
||||
expect(getAgentDotState(failed)).toBe('failed')
|
||||
expect(buildSummaryAgentGroups([failed]).map((group) => group.state)).toEqual(['failed'])
|
||||
expect(summarizeAgents([failed], 'Agent')).toBe('Agent failed')
|
||||
expect(getAgentDotState(running)).toBe('waiting')
|
||||
expect(buildSummaryAgentGroups([running]).map((group) => group.state)).toEqual(['waiting'])
|
||||
})
|
||||
|
||||
it('presents passive working as monitoring', () => {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Which of a session's child records each surface lists. The store keeps every record; these pick
|
||||
// from them on each read, so nothing here is stored and nothing can disagree with the store.
|
||||
//
|
||||
// The sidebar (the status summary) lists running children only; a finished child stays in the
|
||||
// chat's strip, which lists every running child and then the newest finished ones.
|
||||
|
||||
import type { AgentChildWorkView } from './agent-status-child-work-view'
|
||||
|
||||
/** The strip's row budget. Running children always show, even past it. */
|
||||
export const STRUCTURED_STRIP_CHILD_WORK_LIMIT = 100
|
||||
|
||||
/** Settled children that still own live work: they read as monitoring, not finished, and keep
|
||||
* that work's owner on screen. */
|
||||
function settledOwnersOfLiveWork(views: readonly AgentChildWorkView[]): Set<string> {
|
||||
const byId = new Map(views.map((view) => [view.id, view]))
|
||||
const owners = new Set<string>()
|
||||
for (const view of views) {
|
||||
if (view.membership !== 'live') {
|
||||
continue
|
||||
}
|
||||
const seen = new Set([view.id])
|
||||
let owner = view.parentChildWorkId ? byId.get(view.parentChildWorkId) : undefined
|
||||
while (owner && !seen.has(owner.id)) {
|
||||
seen.add(owner.id)
|
||||
if (owner.membership === 'settled') {
|
||||
owners.add(owner.id)
|
||||
}
|
||||
owner = owner.parentChildWorkId ? byId.get(owner.parentChildWorkId) : undefined
|
||||
}
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
function isRunning(view: AgentChildWorkView, owners: ReadonlySet<string>): boolean {
|
||||
return view.membership === 'live' || owners.has(view.id)
|
||||
}
|
||||
|
||||
/** The sidebar's children: the running ones, in store order. */
|
||||
export function structuredSidebarChildWork(
|
||||
views: readonly AgentChildWorkView[]
|
||||
): AgentChildWorkView[] {
|
||||
const owners = settledOwnersOfLiveWork(views)
|
||||
return views.filter((view) => isRunning(view, owners))
|
||||
}
|
||||
|
||||
/** The strip's children, in store order: every running child, then the newest finished ones, up to
|
||||
* the row budget in all. */
|
||||
export function structuredStripChildWork(
|
||||
views: readonly AgentChildWorkView[]
|
||||
): AgentChildWorkView[] {
|
||||
if (views.length <= STRUCTURED_STRIP_CHILD_WORK_LIMIT) {
|
||||
return [...views]
|
||||
}
|
||||
const owners = settledOwnersOfLiveWork(views)
|
||||
const running = views.filter((view) => isRunning(view, owners))
|
||||
const room = Math.max(0, STRUCTURED_STRIP_CHILD_WORK_LIMIT - running.length)
|
||||
const newestFinished = new Set(
|
||||
views
|
||||
.filter((view) => !isRunning(view, owners))
|
||||
.sort((a, b) => (b.settledAt ?? b.observedAt) - (a.settledAt ?? a.observedAt))
|
||||
.slice(0, room)
|
||||
.map((view) => view.id)
|
||||
)
|
||||
return views.filter((view) => isRunning(view, owners) || newestFinished.has(view.id))
|
||||
}
|
||||
Reference in New Issue
Block a user