feat(insights): read every card and aggregate at the team scope

The dashboard's row IS the team now: each listed session folds its WHOLE
subagent subtree into itself (all levels, mirroring the Context tab's cost
fold) — subagent-origin sessions never list, so nothing counts twice. The
session card's billed tokens and priced cost (now labeled 团队用量 /
团队费用), the KPI band's volume and spend, the last-7-days usage chart,
and the heatmap all read the family scope: the subtree's cost buckets
merge per row, the daily ledgers merge day by day, and the composition
split proportions each member's OWN composition instead of the root's.
This commit is contained in:
bowenliang123 committed 2026-10-07 13:12:52 +08:00
1 parent 3dc892c221
commit ef8e1d24dd
6 files changed
+264 -60

No files matched your search

+6 -3
View File
@@ -32,7 +32,10 @@ export function makeOverviewCard(kit: ViewKit): (props: OverviewCardProps) => Re
return function OverviewCard(props: OverviewCardProps): ReactElement {
const { row } = props
const timeline = row.timeline
const billed = billedOf(timeline)
// The card's two money figures ride the team scope: the whole subtree's
// billed tokens and priced cost (the donut and the turns stay the
// session's own — the ring reads the session's CURRENT context).
const billed = billedOf(row)
const occupancy = timeline !== null && typeof timeline.contextWindow === 'number' && timeline.contextWindow > 0
? fmtShare(timeline.current.total, timeline.contextWindow)
: null
@@ -91,11 +94,11 @@ export function makeOverviewCard(kit: ViewKit): (props: OverviewCardProps) => Re
<span className="lc-ov-mini-value">{turnsLabel}</span>
</span>
<span className="lc-ov-mini-stat">
<span className="lc-ov-mini-label">{t('tokens.total')}</span>
<span className="lc-ov-mini-label">{t('ov.card.teamUsage')}</span>
<span className="lc-ov-mini-value">{billed === null ? '—' : fmt(billed)}</span>
</span>
<span className="lc-ov-mini-stat">
<span className="lc-ov-mini-label">{t('stats.cost')}</span>
<span className="lc-ov-mini-label">{t('ov.card.teamCost')}</span>
<span className="lc-ov-mini-value">{props.costLabel}</span>
</span>
</span>
+3 -3
View File
@@ -355,9 +355,9 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
}
}
/** One card's priced cost label, or the dash (no book yet, nothing billed, unpriceable model). */
/** One card's priced cost label at the team scope, or the dash (no book yet, nothing billed, unpriceable models). */
function cardCostOf(row: OverviewRow, book: ModelBook | null, currency: CostCurrency): string {
if (row.timeline?.cost === undefined) return '—'
const cost = estimateSessionCost(row.timeline.cost, book, currency)
if (row.familyCost === null) return '—'
const cost = estimateSessionCost(row.familyCost, book, currency)
return cost === null ? '—' : formatCost(cost, currency)
}
+6 -2
View File
@@ -398,7 +398,7 @@ export const DICT_ZH: Record<string, string> = {
'ov.list.search': '搜索标题 / 目录 / 最后消息…',
'ov.list.sortLabel': '排序',
'ov.list.sort.recent': '最近更新',
'ov.list.sort.tokens': 'Token 总用量',
'ov.list.sort.tokens': '团队用量',
'ov.list.sort.context': '当前 Context',
'ov.list.empty': '还没有会话 — 发起一轮对话后,这里会列出每个会话的上下文画像',
'ov.list.noMatch': '没有匹配当前筛选条件的会话',
@@ -414,6 +414,8 @@ export const DICT_ZH: Record<string, string> = {
'ov.running': '运行中',
'ov.current': '当前',
'ov.card.turns': '{n} 轮 {s} 步',
'ov.card.teamUsage': '团队用量',
'ov.card.teamCost': '团队费用',
'ov.card.lastUser': '最后消息',
'ov.time.now': '刚刚',
'ov.time.m': '{n} 分钟前',
@@ -812,7 +814,7 @@ export const DICT_EN: Record<string, string> = {
'ov.list.search': 'Search title / path / last message…',
'ov.list.sortLabel': 'Sort',
'ov.list.sort.recent': 'Last updated',
'ov.list.sort.tokens': 'Total Tokens',
'ov.list.sort.tokens': 'Team Usage',
'ov.list.sort.context': 'Current Context',
'ov.list.empty': 'No sessions yet — each session’s context profile appears here once you start one',
'ov.list.noMatch': 'No sessions match the current filters',
@@ -828,6 +830,8 @@ export const DICT_EN: Record<string, string> = {
'ov.running': 'Running',
'ov.current': 'current',
'ov.card.turns': '{n} turns · {s} steps',
'ov.card.teamUsage': 'Team Usage',
'ov.card.teamCost': 'Team Cost',
'ov.card.lastUser': 'Last message',
'ov.time.now': 'just now',
'ov.time.m': '{n}m ago',
+128 -38
View File
@@ -6,6 +6,12 @@
* daily ledger) — so the overview joins every session's insight WITHOUT
* opening a single session log.
*
* The row IS the team: subagent-origin sessions never list (the workspace
* browser's own exclusion), so every row folds its WHOLE descendant subtree
* into itself — the card's billed tokens and cost, the KPI band, the usage
* chart, and the heatmap all read the family scope, each member contributing
* its own composition and daily ledger, nothing counted twice.
*
* The list is harness data (untrusted at the boundary): the snapshot is
* re-proved field by field, every projection value passes its services.ts
* sanitizer, and each row's derivation is isolated — one hostile row drops
@@ -19,6 +25,7 @@ import { billedParts } from './categories'
import { cacheHitPercent } from './format'
import { estimateSessionCost, mergeCostUsage } from './cost'
import type { CostCurrency, ModelBook } from './cost'
import { agentRowOf } from './agentTree'
import { activityOf, asRecord, timelineOf, type ClientCtx, type SessionsFace } from './services'
import type { ContextActivity, ContextTimeline, SessionCostUsage, TimingTotals, TokenUsage, ToolTimingTotals } from '../shared/types'
@@ -34,8 +41,17 @@ export interface OverviewRow {
current: boolean
/** The sanitized timeline head, or null when the host folded nothing for this session yet. */
timeline: ContextTimeline | null
/** The sanitized daily ledger, or null on an older host (the heatmap's empty note). */
/**
* The TEAM's merged daily ledger (this session's own plus every descendant
* subagent's, day by day), or null when no member carries one. Subagent-origin
* sessions never list (the workspace browser's exclusion, see rowsOfSnapshot),
* so each family folds into exactly one row and nothing counts twice.
*/
activity: ContextActivity | null
/** Every subtree member's sanitized head, self first, ALL levels — the parts split proportions each member's OWN composition. */
family: (ContextTimeline | null)[]
/** The subtree's merged billed usage — the card stat, KPI band, and token sort's family figure (null: nothing billed anywhere). */
familyCost: SessionCostUsage | null
}
/**
@@ -217,14 +233,85 @@ export function rowsOfSnapshot(snapshot: unknown, workspaces?: unknown): Overvie
current: id === current,
timeline: timelineOf(values?.contextTimeline),
activity: activityOf(values?.contextActivity),
family: [],
familyCost: null,
})
} catch {
// A hostile row (throwing accessor) drops whole; the list keeps working.
}
}
// The family fold: every listed row owns its WHOLE subagent subtree (all
// levels — the walk mirrors the Context tab's cost fold, agentTree
// subagentCostFoldOf: childrenOf off non-blank rows' parentId, BFS with a
// seen-set so a lineage cycle cannot loop). Each member's own sanitized
// timeline and ledger join the row's family; the row's own `activity` then
// becomes the team's merged ledger.
const childrenOf = new Map<string, string[]>()
for (const id of Object.keys(byId)) {
// The row loop's own isolation, mirrored: a hostile (throwing) row drops
// out of the lineage walk the same way it dropped out of the list.
try {
const row = agentRowOf(byId[id])
if (row === null || row.blank || row.parentId === undefined) continue
const list = childrenOf.get(row.parentId) ?? []
list.push(id)
childrenOf.set(row.parentId, list)
} catch { /* the hostile row links nobody */ }
}
for (const row of rows) {
const timelines: (ContextTimeline | null)[] = [row.timeline]
const activities: (ContextActivity | null)[] = [row.activity]
const seen = new Set<string>([row.id])
const queue = [row.id]
for (let i = 0; i < queue.length; i++) {
for (const kid of childrenOf.get(queue[i]) ?? []) {
if (seen.has(kid)) continue
seen.add(kid)
const values = asRecord(asRecord(byId[kid])?.projectionValues)
timelines.push(timelineOf(values?.contextTimeline))
activities.push(activityOf(values?.contextActivity))
queue.push(kid)
}
}
row.family = timelines
row.familyCost = mergeCostUsage(...timelines.map(t => t?.cost))
row.activity = mergeActivity(activities)
}
return rows
}
/**
* Merge several sessions' daily ledgers into one family's: per day the tokens
* and requests sum and the pricing records merge (each member's own entry,
* same day — the fee prices off the same book downstream either way). Null
* when no member carries a ledger, matching a lone session's day-less state.
*/
function mergeActivity(members: (ContextActivity | null)[]): ContextActivity | null {
const days: ContextActivity['days'] = {}
// Widened honestly: a Record index read can miss at runtime.
const byKey: Record<string, ContextActivity['days'][string] | undefined> = days
let any = false
for (const activity of members) {
if (activity === null) continue
any = true
for (const key of Object.keys(activity.days)) {
const entry = activity.days[key]
const prev = byKey[key]
if (prev === undefined) {
days[key] = { tokens: entry.tokens, requests: entry.requests, ...(entry.cost !== undefined ? { cost: entry.cost } : {}) }
} else {
prev.tokens += entry.tokens
prev.requests += entry.requests
if (entry.cost !== undefined) {
/* v8 ignore next 1 -- a merge with a defined input never comes back empty. */
prev.cost = mergeCostUsage(prev.cost, entry.cost) ?? entry.cost
}
}
}
}
return any ? { days } : null
}
// ---- range / filter / sort -------------------------------------------------
export type OverviewRange = '24h' | '7d' | '30d' | 'all'
@@ -240,12 +327,12 @@ export function rangeStartOf(range: OverviewRange, now: number): number | null {
export type OverviewSort = 'recent' | 'tokens' | 'context'
/**
* The session's cumulative billed tokens (the host-folded cost buckets'
* sum), or null when nothing was billed yet — the sort and the card stat
* share this one figure.
* The team card's cumulative billed tokens (the subtree's merged cost
* buckets' sum), or null when nothing was billed anywhere — the sort and the
* card stat share this one figure.
*/
export function billedOf(timeline: ContextTimeline | null): number | null {
const totals = usageTotalsOf(timeline?.cost)
export function billedOf(row: OverviewRow): number | null {
const totals = usageTotalsOf(row.familyCost)
return totals?.total ?? null
}
@@ -304,7 +391,7 @@ export function filterRows(
/** Order the filtered rows; the input array is never mutated. */
export function sortRows(rows: readonly OverviewRow[], sort: OverviewSort): OverviewRow[] {
const copy = [...rows]
if (sort === 'tokens') copy.sort((a, b) => (billedOf(b.timeline) ?? -1) - (billedOf(a.timeline) ?? -1))
if (sort === 'tokens') copy.sort((a, b) => (billedOf(b) ?? -1) - (billedOf(a) ?? -1))
else if (sort === 'context') copy.sort((a, b) => (b.timeline?.current.total ?? -1) - (a.timeline?.current.total ?? -1))
else copy.sort((a, b) => b.updatedAt - a.updatedAt)
return copy
@@ -430,13 +517,13 @@ export interface TokenPartTotal {
/**
* The range's billed tokens split by WHAT they are — the Context tab's Token
* card categorization, folded across sessions: each session's
* `billedParts` estimate (the composition ratios proportioning that
* session's provider-reported prompt total, output exact) sums by category,
* and every session's parts total its own billed figure, so the aggregate's
* card categorization, folded across the rows' FAMILIES: every subtree
* member's `billedParts` estimate (its OWN composition ratios proportioning
* its own provider-reported prompt total, output exact) sums by category,
* and every member's parts total its own billed figure, so the aggregate's
* total stays the exact merged billed volume while the split inherits the
* per-session card's `≈` estimate convention. Null when no session billed
* anything. A non-finite part estimate (a hostile fast-path composition)
* per-session card's `≈` estimate convention. Null when nothing billed
* anywhere. A non-finite part estimate (a hostile fast-path composition)
* drops whole instead of poisoning the sums.
*/
export function tokenPartsOf(rows: readonly OverviewRow[]): { parts: TokenPartTotal[]; total: number } | null {
@@ -444,26 +531,27 @@ export function tokenPartsOf(rows: readonly OverviewRow[]): { parts: TokenPartTo
let total = 0
let any = false
for (const row of rows) {
const timeline = row.timeline
if (timeline === null) continue
const totals = usageTotalsOf(timeline.cost)
if (totals === null) continue
any = true
const usage: TokenUsage = {
uncachedInputTokens: totals.input,
cacheReadTokens: totals.cacheRead,
cacheWriteTokens: totals.cacheWrite,
outputTokens: totals.output,
}
for (const part of billedParts(timeline.current, null, usage)) {
if (!Number.isFinite(part.value) || part.value <= 0) continue
total += part.value
// Widened honestly: a Map get can miss at runtime.
const sumsGet: Map<string, TokenPartTotal | undefined> = sums
const prev = sumsGet.get(part.key)
sums.set(part.key, prev === undefined
? { key: part.key, color: part.color, value: part.value }
: { ...prev, value: prev.value + part.value })
for (const member of row.family) {
if (member === null) continue
const totals = usageTotalsOf(member.cost)
if (totals === null) continue
any = true
const usage: TokenUsage = {
uncachedInputTokens: totals.input,
cacheReadTokens: totals.cacheRead,
cacheWriteTokens: totals.cacheWrite,
outputTokens: totals.output,
}
for (const part of billedParts(member.current, null, usage)) {
if (!Number.isFinite(part.value) || part.value <= 0) continue
total += part.value
// Widened honestly: a Map get can miss at runtime.
const sumsGet: Map<string, TokenPartTotal | undefined> = sums
const prev = sumsGet.get(part.key)
sums.set(part.key, prev === undefined
? { key: part.key, color: part.color, value: part.value }
: { ...prev, value: prev.value + part.value })
}
}
}
if (!any) return null
@@ -508,7 +596,9 @@ export function kpisOf(
book: ModelBook | null | undefined,
currency: CostCurrency,
): OverviewKpis {
const usage = mergeCostUsage(...rows.map(row => row.timeline?.cost))
// The KPI band's billed volume and spend ride the same family scope as the
// cards and the chart: each row's subtree-merged usage.
const usage = mergeCostUsage(...rows.map(row => row.familyCost))
const totals = usageTotalsOf(usage)
let turns = 0
let toolCalls = 0
@@ -518,12 +608,11 @@ export function kpisOf(
let costSessions = 0
let usageSessions = 0
for (const row of rows) {
const cost = row.timeline?.cost
// Each qualifying sub-line counts the sessions its own figure covers: a
// session with usage but no book rates feeds the cache-hit rate while
// pricing to nothing.
if (estimateSessionCost(cost, book, currency) !== null) costSessions++
if (usageTotalsOf(cost) !== null) usageSessions++
if (estimateSessionCost(row.familyCost, book, currency) !== null) costSessions++
if (usageTotalsOf(row.familyCost) !== null) usageSessions++
turns += turnsOf(row.timeline)
const timing = row.timeline?.timing
toolCalls += timing?.toolCalls ?? 0
@@ -563,7 +652,8 @@ export interface DayTotals {
/**
* Merge every row's daily ledger into one — the heatmap's and the usage
* chart's data. A session counts toward a day only when its own entry
* chart's data (each row's ledger is already its family's merge, see
* rowsOfSnapshot). A row counts toward a day only when its family entry
* carries activity, mirroring the day filter's predicate, so the cell's
* tooltip previews the click; a zeroed entry is skipped whole. Each day's
* pricing records merge into one SessionCostUsage and price off the SAME
+10 -6
View File
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { makeOverviewCard } from '../../../src/client/components/overviewCard'
import type { OverviewRow } from '../../../src/client/overview'
import type { ContextActivity, ContextTimeline } from '../../../src/shared/types'
import type { ContextActivity, ContextTimeline, SessionCostUsage } from '../../../src/shared/types'
import { click, makeKit, mount, query, queryAll, text } from '../helpers/kit'
const kit = makeKit()
@@ -23,21 +23,25 @@ function rowOf(over: Partial<OverviewRow> = {}): OverviewRow {
current: false,
timeline: null,
activity: null,
family: [over.timeline ?? null],
familyCost: null,
...over,
}
}
const FAMILY_COST: SessionCostUsage = { deepseek: { m: { peak: { uncached: 1000, cacheRead: 500, cacheWrite: 0, output: 250 } } } }
const TIMELINE = {
current: { system: 100, tools: 50, user: 30, inject: 10, skill: 10, assistant: 200, tool: 100, total: 500 },
contextWindow: 1000,
requests: [{}, {}],
counts: { turns: 9, steps: 12, injects: 1, compactions: 0, prunes: 0 },
cost: { deepseek: { m: { peak: { uncached: 1000, cacheRead: 500, cacheWrite: 0, output: 250 } } } },
cost: FAMILY_COST,
} as unknown as ContextTimeline
describe('OverviewCard', () => {
test('a folded session renders the donut, occupancy, and the three figures', async () => {
const m = await mount(h(Card, { row: rowOf({ timeline: TIMELINE }), costLabel: '$0.02', now: NOW, onOpen: () => {} }))
const m = await mount(h(Card, { row: rowOf({ timeline: TIMELINE, familyCost: FAMILY_COST }), costLabel: '$0.02', now: NOW, onOpen: () => {} }))
const card = query(m.container, 'button.lc-ov-session')
assert.ok(!card.className.includes('lc-ov-session-current'))
assert.equal(query(m.container, '.lc-ov-session-title').textContent, 'refactor the parser')
@@ -45,10 +49,10 @@ describe('OverviewCard', () => {
// The mini donut: center carries the current total and the window share.
assert.equal(query(m.container, '.lc-donut-center b').textContent, '500')
assert.equal(query(m.container, '.lc-donut-center span').textContent, '50.0%')
// The three mini stats: turns+steps lead, then total billed, then cost.
// The three mini stats: turns+steps lead, then the team's billed, then its cost.
const labels = queryAll(m.container, '.lc-ov-mini-label').map(el => el.textContent)
const values = queryAll(m.container, '.lc-ov-mini-value').map(el => el.textContent)
assert.deepEqual(labels, ['Turns', 'Total', 'Cost'])
assert.deepEqual(labels, ['Turns', 'Team Usage', 'Team Cost'])
assert.deepEqual(values, ['9 turns · 12 steps', '1.8k', '$0.02'])
await m.unmount()
})
@@ -170,7 +174,7 @@ describe('OverviewCard', () => {
const ZhCard = makeOverviewCard(makeKit('zh'))
const m = await mount(h(ZhCard, { row: rowOf({ timeline: TIMELINE }), costLabel: '¥0.13', now: NOW, onOpen: () => {} }))
const labels = queryAll(m.container, '.lc-ov-mini-label').map(el => el.textContent)
assert.deepEqual(labels, ['轮次', '总用量', '费用'])
assert.deepEqual(labels, ['轮次', '团队用量', '团队费用'])
const values = queryAll(m.container, '.lc-ov-mini-value').map(el => el.textContent)
assert.equal(values[0], '9 轮 12 步')
assert.equal(query(m.container, '.lc-ov-session-time').textContent, '3 小时前')
+111 -8
View File
@@ -32,7 +32,7 @@ import {
} from '../../src/client/overview'
import type { ClientCtx } from '../../src/client/services'
import type { ContextActivity, ContextTimeline, SessionCostUsage } from '../../src/shared/types'
import { priceIndexOf } from '../../src/client/cost'
import { mergeCostUsage, priceIndexOf } from '../../src/client/cost'
/** The minimal wire-valid timeline head, overridable per case. */
function timelineOf(over: Record<string, unknown> = {}): Record<string, unknown> {
@@ -71,6 +71,8 @@ function rowOf(over: Partial<OverviewRow> = {}): OverviewRow {
current: false,
timeline: null,
activity: null,
family: [over.timeline ?? null],
familyCost: null,
...over,
}
}
@@ -118,7 +120,7 @@ describe('rowsOfSnapshot', () => {
const [b, c, d, e] = rows
assert.deepEqual(b, {
id: 'b', title: 'shown', updatedAt: 5, running: true, current: true,
timeline: null, activity: null,
timeline: null, activity: null, family: [null], familyCost: null,
})
assert.equal(c.title, 'titled')
assert.equal(c.current, false)
@@ -176,10 +178,9 @@ describe('rowsOfSnapshot', () => {
})
describe('billedOf / turnsOf', () => {
test('billed sums the cost buckets; absent cost reads null', () => {
assert.equal(billedOf(null), null)
assert.equal(billedOf({ current: 1 } as never), null)
assert.equal(billedOf({ cost: COST } as never), 200)
test('billed sums the team’s merged cost buckets; absent cost reads null', () => {
assert.equal(billedOf(rowOf()), null)
assert.equal(billedOf(rowOf({ familyCost: COST })), 200)
})
test('turns prefer the precomputed count and fall back to the records', () => {
@@ -246,7 +247,7 @@ describe('filterRows', () => {
})
describe('sortRows', () => {
const cheap = rowOf({ id: 'cheap', updatedAt: 1, timeline: { current: { total: 50 }, cost: COST } as unknown as ContextTimeline })
const cheap = rowOf({ id: 'cheap', updatedAt: 1, timeline: { current: { total: 50 }, cost: COST } as unknown as ContextTimeline, familyCost: COST })
const dear = rowOf({
id: 'dear',
updatedAt: 2,
@@ -254,6 +255,7 @@ describe('sortRows', () => {
current: { total: 900 },
cost: { deepseek: { m: { peak: { uncached: 1000, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
} as unknown as ContextTimeline,
familyCost: { deepseek: { m: { peak: { uncached: 1000, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
})
const plain = rowOf({ id: 'plain', updatedAt: 3 })
@@ -446,6 +448,7 @@ describe('kpisOf', () => {
requests: [],
timing: { wallMs: 90_000, ttftMs: 1_000, genMs: 30_000, calls: 4, toolsMs: 20_000, toolCalls: 7, tools: {} },
} as unknown as ContextTimeline,
familyCost: COST,
}),
rowOf({
timeline: {
@@ -476,13 +479,14 @@ describe('kpisOf', () => {
test('a session with usage the book cannot price feeds the cache-hit rate but prices to nothing', () => {
const rows = [
rowOf({ timeline: { cost: COST, current: { system: 100, tools: 50, user: 30, inject: 10, skill: 10, assistant: 200, tool: 100, total: 500 }, requests: [] } as unknown as ContextTimeline }),
rowOf({ timeline: { cost: COST, current: { system: 100, tools: 50, user: 30, inject: 10, skill: 10, assistant: 200, tool: 100, total: 500 }, requests: [] } as unknown as ContextTimeline, familyCost: COST }),
rowOf({
timeline: {
cost: { openai: { 'gpt-5': { peak: { uncached: 10, cacheRead: 5, cacheWrite: 1, output: 2 } } } },
current: { system: 10, tools: 0, user: 0, inject: 0, skill: 0, assistant: 0, tool: 0, total: 10 },
requests: [],
} as unknown as ContextTimeline,
familyCost: { openai: { 'gpt-5': { peak: { uncached: 10, cacheRead: 5, cacheWrite: 1, output: 2 } } } },
}),
rowOf(),
]
@@ -593,6 +597,105 @@ describe('rowsOfSnapshot: subagent rows', () => {
})
})
describe('rowsOfSnapshot: the family fold', () => {
const COST2: SessionCostUsage = { zhipuai: { m: { peak: { uncached: 300, cacheRead: 0, cacheWrite: 0, output: 0 } } } }
/** A minimal head carrying the given billed usage (and a user-only composition). */
function billedHead(tokens: number, cost?: SessionCostUsage): Record<string, unknown> {
return timelineOf({
current: { system: 0, tools: 0, user: tokens, inject: 0, skill: 0, assistant: 0, tool: 0, total: tokens },
...(cost !== undefined ? { cost } : {}),
})
}
test('every descendant level folds into the root row’s family — members, cost, and days merge', () => {
const rows = rowsOfSnapshot(snapshotOf([
['root', {
displayTitle: 'root',
projectionValues: {
contextTimeline: billedHead(100, COST),
contextActivity: { days: { '2026-09-16': { tokens: 5, requests: 1, cost: COST } } },
},
}],
['kid', {
origin: 'subagent', parentId: 'root',
projectionValues: {
contextTimeline: billedHead(50, COST2),
contextActivity: { days: { '2026-09-16': { tokens: 7, requests: 2, cost: COST2 } } },
},
}],
['grand', {
origin: 'subagent', parentId: 'kid',
projectionValues: {
contextTimeline: billedHead(25),
contextActivity: { days: { '2026-09-16': { tokens: 4, requests: 1 }, '2026-09-15': { tokens: 3, requests: 1 } } },
},
}],
['stranger', { displayTitle: 'unrelated' }],
]))
assert.ok(rows !== null)
assert.deepEqual(rows.map(r => r.id), ['root', 'stranger'], 'subagent-origin rows stay unlisted')
const [root, stranger] = rows
assert.equal(root.family.length, 3, 'self + kid + grand — all levels')
// COST (200) + COST2 (300) merge; the grandchild carried no cost.
assert.equal(billedOf(root), 500)
assert.equal(billedOf(stranger), null)
// The daily ledgers merge across the subtree, pricing records included.
assert.equal(root.activity?.days['2026-09-16'].tokens, 16)
assert.equal(root.activity?.days['2026-09-16'].requests, 4)
assert.deepEqual(root.activity?.days['2026-09-16'].cost, mergeCostUsage(COST, COST2))
assert.equal(root.activity?.days['2026-09-15'].tokens, 3)
assert.equal(root.activity?.days['2026-09-15'].cost, undefined)
// The composed split proportions each member's OWN composition: the root's
// all-user 160 billed input + output 40, the kid's all-user 300 — user 460 / output 40.
const parts = tokenPartsOf([root])
assert.deepEqual(parts, {
total: 500,
parts: [
{ key: 'user', color: 'var(--color-green-500)', value: 460 },
{ key: 'output', color: 'var(--color-pink-500)', value: 40 },
],
})
})
test('an unlisted subtree (a subagent whose root never lists) leaks nowhere', () => {
const rows = rowsOfSnapshot(snapshotOf([
['root', { displayTitle: 'root', projectionValues: { contextTimeline: billedHead(1, COST) } }],
['ghost', { origin: 'subagent', parentId: 'missing', projectionValues: { contextTimeline: billedHead(999, COST2) } }],
]))
assert.ok(rows !== null)
assert.equal(rows.length, 1)
assert.equal(billedOf(rows[0]), 200, 'the ghost subtree folds into no row')
})
test('a blank placeholder’s subtree stays out, and a lineage cycle cannot loop the walk', () => {
const rows = rowsOfSnapshot(snapshotOf([
// The reachable cycle: root points at p and p points back — the walk
// must cut at the revisited id (root counts its family, never itself twice).
['root', { displayTitle: 'root', parentId: 'p', projectionValues: { contextTimeline: billedHead(1, COST) } }],
['p', { origin: 'subagent', parentId: 'root', projectionValues: { contextTimeline: billedHead(2, COST2) } }],
['blank', { blank: true, parentId: 'root', projectionValues: { contextTimeline: billedHead(9, COST2) } }],
['under-blank', { origin: 'subagent', parentId: 'blank', projectionValues: { contextTimeline: billedHead(9, COST2) } }],
]))
assert.ok(rows !== null)
assert.equal(rows.length, 1)
assert.equal(rows[0].family.length, 2, 'root + p once; the blank is no agent and its child never links through')
assert.equal(billedOf(rows[0]), 500, 'COST + COST2 — the cycle’s revisit adds nothing twice')
})
test('hostile descendant projections degrade to null members, never a throw', () => {
const rows = rowsOfSnapshot(snapshotOf([
['root', { displayTitle: 'root', projectionValues: { contextTimeline: billedHead(1, COST) } }],
['kid', { origin: 'subagent', parentId: 'root', projectionValues: { contextTimeline: 7, contextActivity: 'x' } }],
['junk', 5 as unknown as Record<string, unknown>],
]))
assert.ok(rows !== null)
assert.equal(rows[0].family.length, 2)
assert.equal(rows[0].family[1], null, 'the hostile head sanitizes to null in place')
assert.equal(rows[0].activity, null, 'no member carried a ledger → the family has none')
assert.equal(billedOf(rows[0]), 200)
})
})
describe('workspacesSnapshotOf', () => {
test('a non-function or throwing seat reads null; the raw snapshot passes through', () => {
assert.equal(workspacesSnapshotOf({}), null)