round 2: address manual compaction review findings

This commit is contained in:
Hypatia May
2026-07-30 18:15:12 +08:00
parent eda7c76eb1
commit 4de4d693a2
22 changed files with 531 additions and 152 deletions
@@ -27,7 +27,8 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
}
interface TableRecord {
turn: number
turn: number | null
section: number
group: string
groupStart: boolean
turnStart: boolean
@@ -69,7 +70,7 @@ interface ToolCallTextParts {
}
interface SelectedRequest {
turn: number
turn: number | null
number: number
group: string
}
@@ -237,15 +238,12 @@ export interface TrajectoryTableProps {
onToggleAssistant: (index: number) => void
}
/** One request identity paired with its session-global number. */
export interface TrajectoryRequestNumber {
/** Request-inspector fields shared by ordinary generation and compaction. */
interface TrajectoryRequestNumberBase {
/** Request anchor event sequence; absent for the currently streaming ordinary request. */
seq?: number
turn: number
step: number
group: string
number: number
purpose?: 'compaction'
status?: 'complete' | 'running' | 'error'
startedAt?: number
completedAt?: number | null
@@ -261,6 +259,20 @@ export interface TrajectoryRequestNumber {
cumulativeUsage?: TrajectoryUsage
}
/** One purpose-discriminated request identity paired with its session-global number. */
export type TrajectoryRequestNumber = TrajectoryRequestNumberBase & (
| {
purpose?: 'assistant'
turn: number
step: number
}
| {
purpose: 'compaction'
turn: number | null
step: 0
}
)
/** Disjoint provider token buckets for one request or a session prefix. */
export interface TrajectoryUsage {
input?: number
@@ -271,17 +283,18 @@ export interface TrajectoryUsage {
}
function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] {
return turns.flatMap((turn) => {
let firstInTurn = true
return turns.flatMap((turn, section) => {
let firstInSection = true
const records = turn.groups.flatMap((group) => {
return group.cells.map((cell, index) => {
const turnStart = firstInTurn
const turnStart = firstInSection
&& cell.requestOnly !== true
&& cell.kind !== 'system'
&& cell.kind !== 'compacted'
if (turnStart) firstInTurn = false
&& (cell.kind !== 'compacted' || turn.turn === null)
if (turnStart) firstInSection = false
return {
turn: turn.turn,
section,
group: group.title,
groupStart: index === 0,
turnStart,
@@ -305,18 +318,18 @@ function filterRecords(
record.cell.requestOnly !== true && matches.has(record.cell.index),
)
.map(record => ({ ...record, groupStart: false, turnStart: false, turnEnd: false }))
const startedTurns = new Set<number>()
const startedSections = new Set<number>()
for (const [index, record] of filtered.entries()) {
const previous = filtered[index - 1]
const next = filtered[index + 1]
record.groupStart = previous === undefined
|| previous.turn !== record.turn
|| previous.section !== record.section
|| previous.group !== record.group
record.turnStart = !startedTurns.has(record.turn)
record.turnStart = !startedSections.has(record.section)
&& record.cell.kind !== 'system'
&& record.cell.kind !== 'compacted'
if (record.turnStart) startedTurns.add(record.turn)
record.turnEnd = next === undefined || next.turn !== record.turn
&& (record.cell.kind !== 'compacted' || record.turn === null)
if (record.turnStart) startedSections.add(record.section)
record.turnEnd = next === undefined || next.section !== record.section
}
return filtered
}
@@ -327,10 +340,14 @@ function requestStep(group: string): number | undefined {
return Number.isInteger(value) && value > 0 ? value : undefined
}
function requestKey(turn: number, group: string): string {
function requestKey(turn: number | null, group: string): string {
return `${turn}\u0000${group}`
}
function sectionLabel(turn: number | null): string {
return turn === null ? 'Between turns' : `Turn ${turn}`
}
function indexRequestNumbers(
records: readonly TableRecord[],
sessionNumbers: readonly TrajectoryRequestNumber[] | undefined,
@@ -372,12 +389,13 @@ function collapseTurnRecords(
if (collapsedTurns.size === 0) return [...records]
const recordsByTurn = new Map<number, TableRecord[]>()
for (const record of records) {
if (record.turn === null) continue
const turnRecords = recordsByTurn.get(record.turn) ?? []
turnRecords.push(record)
recordsByTurn.set(record.turn, turnRecords)
}
return records.flatMap((record) => {
if (!collapsedTurns.has(record.turn)) return [record]
if (record.turn === null || !collapsedTurns.has(record.turn)) return [record]
const turnRecords = recordsByTurn.get(record.turn) ?? [record]
if (record.cell.requestOnly === true || record.cell.kind === 'system') return [record]
const contentRecords = turnRecords.filter(candidate =>
@@ -1486,7 +1504,7 @@ export function TrajectoryTable({
const selectedRequestCumulativeUsage =
selectedRequestInfo?.cumulativeUsage ?? selectedRequestUsage
const selectedRequestOptions = selectedRequestInfo?.requestConfig
const activeTurn = selectedRequest?.turn ?? selected?.turn
const activeTurn = selectedRequest === null ? selected?.turn : selectedRequest.turn
const selectedTabs = selectedRequest !== null
? REQUEST_TABS.filter(tab => tab.id !== 'options' || selectedRequestOptions !== undefined)
: selected === undefined ? [] : detailTabs(selected)
@@ -1554,7 +1572,7 @@ export function TrajectoryTable({
const openRecordSummary = (target: TableRecord) => {
const targetAt = allRecords.findIndex(record => record.cell.index === target.cell.index)
if (collapsedTurns.has(target.turn)) onToggleTurn(target.turn)
if (target.turn !== null && collapsedTurns.has(target.turn)) onToggleTurn(target.turn)
if (target.cell.kind === 'tool' || target.cell.kind === 'subtool') {
for (let i = targetAt - 1; i >= 0; i--) {
const candidate = allRecords[i]
@@ -1600,7 +1618,7 @@ export function TrajectoryTable({
&& record.cell.index === allRecords[0]?.cell.index
const request = record.groupStart
&& !isCollapsedSummary
&& !collapsedTurns.has(record.turn)
&& (record.turn === null || !collapsedTurns.has(record.turn))
? requestNumbers.get(requestKey(record.turn, record.group))
: undefined
const requestInfo = request === undefined
@@ -1641,13 +1659,14 @@ export function TrajectoryTable({
? undefined
: isCollapsedSummary
? () => {
if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn)
else onToggleAssistant(record.cell.index)
if (record.collapsedSummaryKind === 'turn' && record.turn !== null) {
onToggleTurn(record.turn)
} else onToggleAssistant(record.cell.index)
}
: () => { selectRecord(record.cell.index) }}
onDoubleClick={(event) => {
if (isCollapsedSummary || isRequestOnly) return
if (collapsedTurns.has(record.turn)) {
if (record.turn !== null && collapsedTurns.has(record.turn)) {
event.preventDefault()
onToggleTurn(record.turn)
return
@@ -1661,6 +1680,7 @@ export function TrajectoryTable({
return
}
if (!record.turnStart) return
if (record.turn === null) return
if (allRecords.filter(candidate =>
candidate.turn === record.turn
&& candidate.cell.requestOnly !== true
@@ -1673,8 +1693,9 @@ export function TrajectoryTable({
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
if (isCollapsedSummary) {
if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn)
else onToggleAssistant(record.cell.index)
if (record.collapsedSummaryKind === 'turn' && record.turn !== null) {
onToggleTurn(record.turn)
} else onToggleAssistant(record.cell.index)
return
}
selectRecord(record.cell.index)
@@ -1701,7 +1722,9 @@ export function TrajectoryTable({
onDoubleClick={(event) => { event.stopPropagation() }}
/>
)}
{activeTurn === record.turn && !isInitialSystem && (
{record.turn !== null
&& activeTurn === record.turn
&& !isInitialSystem && (
<span className={css.turnRail} aria-hidden="true" />
)}
{!isCollapsedSummary && selectedIndex === record.cell.index && (
@@ -1715,7 +1738,7 @@ export function TrajectoryTable({
? `${css.turnLabel} ${css.turnLabelActive}`
: css.turnLabel}
>
Turn {record.turn}
{sectionLabel(record.turn)}
</span>
)}
<div className={css.eventInner}>
@@ -1895,8 +1918,8 @@ export function TrajectoryTable({
</span>
<span className={css.detailsLocation}>
{selectedRequestInfo?.purpose === 'compaction'
? `Compaction · Turn ${selectedRequest.turn}`
: `Turn ${selectedRequest.turn}`}
? `Compaction · ${sectionLabel(selectedRequest.turn)}`
: sectionLabel(selectedRequest.turn)}
</span>
</>
)
@@ -1927,8 +1950,8 @@ export function TrajectoryTable({
</span>
<span className={css.detailsLocation}>
{selected.cell.kind === 'compacted'
? `Turn ${selected.turn}`
: `Turn ${selected.turn} · ${selected.group}`}
? sectionLabel(selected.turn)
: `${sectionLabel(selected.turn)} · ${selected.group}`}
</span>
</>
)}
@@ -315,9 +315,9 @@ export const TrajectoryTimeline = memo(function TrajectoryTimeline({
)}
<div className={css.turnBoundaries} aria-hidden="true">
{model.turnBoundaries
.slice(1)
.filter(boundary =>
boundary.time >= domainStart
boundary.time > model.start
&& boundary.time >= domainStart
&& boundary.time <= domainStart + domainDuration)
.map(boundary => (
<span
@@ -102,7 +102,7 @@ function searchMatches(
...(cell.outputBlocks ?? []),
]
const text = [
`turn ${turn.turn}`,
turn.turn === null ? 'between turns' : `turn ${turn.turn}`,
group.title,
cell.kind,
cell.kind === 'message' ? 'assistant' : undefined,
@@ -380,13 +380,15 @@ export function TrajectoryView({
const collapsibleTurnIds = useMemo(
() => turns
.filter(turn =>
turn.turn !== null
&&
turn.groups.reduce(
(count, group) =>
count + group.cells.filter(cell =>
cell.requestOnly !== true && cell.kind !== 'system').length,
0,
) > 1)
.map(turn => turn.turn),
.flatMap(turn => turn.turn === null ? [] : [turn.turn]),
[turns],
)
const allTurnsCollapsed = collapsibleTurnIds.length > 0
@@ -107,6 +107,8 @@ export function trajectoryBranchContainsRequest(
request.resultSeq !== undefined
&& branch.retainedSurfaceSeqs.has(request.resultSeq)
) || (
request.purpose === 'compaction'
&&
request.replacementSeq !== undefined
&& branch.retainedSurfaceSeqs.has(request.replacementSeq)
)
@@ -24,9 +24,9 @@ export interface TrajectoryGroupModel {
cells: readonly TrajectoryCellProps[]
}
/** One sticky-turn section. */
/** One sticky turn, or a standalone compaction section between turns. */
export interface TrajectoryTurnModel {
turn: number
turn: number | null
groups: readonly TrajectoryGroupModel[]
}
@@ -66,6 +66,9 @@ interface TurnBucket {
groups: LaidGroup[]
}
type AssistantRequestView = Extract<RequestView, { purpose: 'assistant' }>
type CompactionRequestView = Extract<RequestView, { purpose: 'compaction' }>
type InputNode = Extract<
ConversationSnapshot['nodes'][number],
{ kind: 'user' | 'steering' | 'context' }
@@ -81,18 +84,18 @@ type OrderedLayoutEntry =
| {
kind: 'compaction'
seq: number
request: RequestView
request: CompactionRequestView
}
| {
kind: 'system'
seq: number
request: RequestView
request: AssistantRequestView
change: RequestPromptChange
}
| {
kind: 'request'
seq: number
request: RequestView
request: AssistantRequestView
}
function layoutEntryOrder(entry: OrderedLayoutEntry): number {
@@ -136,6 +139,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
if (startedAt !== null) callStartById.set(call.callId, startedAt)
}
const turns = new Map<number, TurnBucket>()
const standaloneCompactions: TurnBucket[] = []
let index = 0
let prevAbsTime: number | null = null
let lastAssistantTurn: number | null = null
@@ -191,13 +195,16 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
nodeIndex,
})),
...requests
.filter(request => request.purpose === 'compaction')
.filter((request): request is CompactionRequestView =>
request.purpose === 'compaction')
.map(request => ({
kind: 'compaction' as const,
seq: request.startSeq,
request,
})),
...requests.flatMap(request => request.promptChange === undefined || request.prompt === undefined
...requests.flatMap(request => request.purpose !== 'assistant'
|| request.promptChange === undefined
|| request.prompt === undefined
? []
: [{
kind: 'system' as const,
@@ -206,7 +213,8 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
change: request.promptChange,
}]),
...requests
.filter(request => request.purpose === 'assistant')
.filter((request): request is AssistantRequestView =>
request.purpose === 'assistant')
.filter(request =>
!representedRequests.has(`${request.turn}\u0000${request.step}`),
)
@@ -297,13 +305,17 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
startedAt: finiteTime(request.startedAt),
}
attachUsage(cell, request.usage as UsageLike | undefined)
bucket(request.turn).groups.push({
title: `Compaction ${request.startSeq}`,
laid: [{
absTime: finiteTime(request.startedAt),
cell,
const compaction: TurnBucket = {
groups: [{
title: `Compaction ${request.startSeq}`,
laid: [{
absTime: finiteTime(request.startedAt),
cell,
}],
}],
})
}
if (request.turn === null) standaloneCompactions.push(compaction)
else bucket(request.turn).groups.push(...compaction.groups)
prevAbsTime = finiteTime(request.completedAt) ?? finiteTime(request.startedAt) ?? prevAbsTime
continue
}
@@ -446,15 +458,16 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
turns.set(1, first)
}
for (const entry of turns.values()) {
for (const entry of [...turns.values(), ...standaloneCompactions]) {
for (const group of entry.groups) {
for (const laid of group.laid) attachToolSchema(laid, callSchemas)
}
}
return [...turns.entries()]
.sort(([a], [b]) => a - b)
.map(([turn, entry]) => toTurnModel(turn, entry))
return [
...[...turns.entries()].map(([turn, entry]) => toTurnModel(turn, entry)),
...standaloneCompactions.map(entry => toTurnModel(null, entry)),
].sort((left, right) => firstCellIndex(left) - firstCellIndex(right))
}
function attachToolSchema(
@@ -468,7 +481,7 @@ function attachToolSchema(
}
function toTurnModel(
turn: number,
turn: number | null,
entry: TurnBucket,
): TrajectoryTurnModel {
const groups = entry.groups.map(({ title, laid }): TrajectoryGroupModel => {
@@ -482,6 +495,14 @@ function toTurnModel(
return { turn, groups }
}
/** Chronological section position from the fold's monotonically assigned cell indexes. */
function firstCellIndex(turn: TrajectoryTurnModel): number {
return Math.min(
...turn.groups.flatMap(group => group.cells.map(cell => cell.index)),
Number.POSITIVE_INFINITY,
)
}
/** Wall-span duration + tool histogram, e.g. `1.5 s bash×6`. */
function groupDescription(laid: readonly LaidCell[]): string | undefined {
const parts: string[] = []
@@ -86,10 +86,12 @@ export function deriveTrajectoryTimeline(
group.cells.filter(cell => cell.requestOnly !== true),
)
if (cells.length === 0) continue
turnBoundaries.push({
turn: turn.turn,
time: spans.length,
})
if (turn.turn !== null) {
turnBoundaries.push({
turn: turn.turn,
time: spans.length,
})
}
spans.push(...cells.map((cell, offset): TrajectoryTimelineSpan => ({
start: spans.length + offset,
end: spans.length + offset + 1,
@@ -147,10 +149,12 @@ function deriveTimedTimeline(
start: span.start - removedUserIdle,
end: (actualDuration ? span.end : span.start) - removedUserIdle,
})))
turnBoundaries.push({
turn: turn.turn,
time: turnStart - removedUserIdle,
})
if (turn.turn !== null) {
turnBoundaries.push({
turn: turn.turn,
time: turnStart - removedUserIdle,
})
}
previousTurnEnd = previousTurnEnd === null
? turnEnd
: Math.max(previousTurnEnd, turnEnd)