diff --git a/apps/web/src/components/agent-sessions/session-detail/session-transcript.tsx b/apps/web/src/components/agent-sessions/session-detail/session-transcript.tsx
index 2ca94c645..efa976917 100644
--- a/apps/web/src/components/agent-sessions/session-detail/session-transcript.tsx
+++ b/apps/web/src/components/agent-sessions/session-detail/session-transcript.tsx
@@ -79,8 +79,8 @@ const ROW_ESTIMATE = {
tool: 180,
"lane-open": 44,
"lane-close": 34,
- parallel: 46,
- "parallel-turns": 46,
+ parallel: 34,
+ "parallel-turns": 34,
structure: 30,
note: 86,
divider: 60,
@@ -156,12 +156,6 @@ export function SessionTranscript({
useAnimationFrameWithResizeObserver: true,
})
- const indexByKey = useMemo(() => new Map(rows.map((row, index) => [row.key, index])), [rows])
- const jumpTo = (key: string) => {
- const index = indexByKey.get(key)
- if (index !== undefined) virtualizer.scrollToIndex(index, { align: "start" })
- }
-
// A pasted `?span=` link lands on the block it names. Once, on mount — after
// that the URL follows the reader rather than leading them. Not before the
// scroller exists, though: the list element attaches in a layout effect, so
@@ -215,7 +209,6 @@ export function SessionTranscript({
selected={"span" in row && row.span.spanId === selectedSpanId}
onSelectSpan={onSelectSpan}
onOpenTraceView={onOpenTraceView}
- onJump={jumpTo}
/>
)
@@ -237,7 +230,6 @@ interface BlockProps {
selected: boolean
onSelectSpan: (spanId: string | undefined) => void
onOpenTraceView: () => void
- onJump: (key: string) => void
}
/**
@@ -387,19 +379,6 @@ function TurnChapter({
{turn.agentName !== undefined && }
{turn.failed && Failed}
- {/* The cluster's banner and the indentation carry the explanation; the
- pill is the glanceable trace of it that survives collapse. Bounded,
- unlike the old per-turn jump chips, whose row of unshrinkable
- buttons was one of the things pushing the page sideways. */}
- {row.parallelWith.length > 0 && (
- turnOrdinal(ref.turn)).join(", ")}`}
- >
-
- parallel
-
- )}
{/* Shrinkable, unlike its neighbours: the summary joins every tool
name in the turn, so it truncates rather than widening the page. */}
{collapsed && {summariseTurn(row)}}
@@ -595,7 +574,11 @@ function SystemBlock({
{row.laneKind === "subagent" ? "Subagent" : "Agent"}
@@ -1092,17 +1075,6 @@ function LaneOpen({
: `· trace ${row.span.traceId.slice(0, 8)}`}{" "}
· {row.spanCount} spans · {formatDuration(row.span.durationMs)}
- {/* The fork banner above already lists every sibling lane; this is the
- glanceable trace of it that survives past that marker scrolling out. */}
- {row.parallelWith.length > 0 && (
- ref.agentName).join(", ")}`}
- >
-
- parallel
-
- )}
{/* The handoff's own payload: the `execute_tool task` span this block
@@ -1165,53 +1137,26 @@ function LaneClose({
}
/**
- * The fork banner both parallel markers share: a bordered, tinted block whose
- * every line WRAPS. The old markers were one flex line of unshrinkable text and
- * chips, which is exactly the shape that pushes a page into sideways clipping —
- * a marker about concurrency must never cost the reader the right edge.
+ * The fork marker both parallel kinds share: one rule across the column, in the
+ * same shape as the page's other structural lines. WHAT forked is already on
+ * the rows right below it — all the marker has to say is that they did not run
+ * in sequence, and when they were open together.
*/
-function ParallelBanner({
- title,
- description,
- refs,
- onJump,
-}: {
- title: string
- description: string
- /** The forked threads, as jump chips. */
- refs: readonly { key: string; label: string }[]
- onJump: (key: string) => void
-}) {
+function ParallelRule({ label, range }: { label: string; range: string }) {
return (
-
-
-
- {title}
-
-
- {description}
-
-
- {refs.map((ref) => (
-
- ))}
-
+
+
+ {label}
+ {range}
+
)
}
/** Only a window every member shared is reported as an overlap. A chain of
- * pairwise overlaps has none, and the sentence then reports the run's extent
+ * pairwise overlaps has none, and the marker then reports the run's extent
* instead of inventing one. */
-function overlapSentence(
+function overlapWindow(
row: {
startMs: number
endMs: number
@@ -1221,8 +1166,8 @@ function overlapSentence(
timeZone: string,
): string {
return row.overlapStartMs !== undefined && row.overlapEndMs !== undefined
- ? `they overlap ${clockOf(row.overlapStartMs, timeZone)} → ${clockOf(row.overlapEndMs, timeZone)}`
- : `their runs interleave between ${clockOf(row.startMs, timeZone)} and ${clockOf(row.endMs, timeZone)}`
+ ? `overlap ${clockOf(row.overlapStartMs, timeZone)} → ${clockOf(row.overlapEndMs, timeZone)}`
+ : `interleaved ${clockOf(row.startMs, timeZone)} → ${clockOf(row.endMs, timeZone)}`
}
/**
@@ -1232,21 +1177,18 @@ function overlapSentence(
function ParallelMarker({
row,
timeZone,
- onJump,
}: BlockProps & { row: Extract }) {
return (
- ({ key: lane.key, label: lane.agentName }))}
- onJump={onJump}
+
)
@@ -1265,24 +1207,18 @@ function ParallelMarker({
function ParallelTurnsMarker({
row,
timeZone,
- onJump,
}: BlockProps & { row: Extract }) {
return (
- ({
- key: ref.key,
- label: `${turnOrdinal(ref.turn).toUpperCase()}${ref.turn.agentName === undefined ? "" : ` ${ref.turn.agentName}`}`,
- }))}
- onJump={onJump}
+
)
diff --git a/apps/web/src/lib/agent-sessions/session-transcript.test.ts b/apps/web/src/lib/agent-sessions/session-transcript.test.ts
index 52013a874..c6f9957e3 100644
--- a/apps/web/src/lib/agent-sessions/session-transcript.test.ts
+++ b/apps/web/src/lib/agent-sessions/session-transcript.test.ts
@@ -237,18 +237,13 @@ describe("buildTranscript — lanes and parallel markers", () => {
])
})
- it("marks overlapping lanes with reciprocal jump links", () => {
+ it("opens one marker over the lanes that overlapped", () => {
const rows = transcript(twoLanes)
const opens = rows.filter((row) => row.kind === "lane-open")
expect(opens.map((row) => row.agentName)).toEqual(["db-lane", "trace-lane"])
- expect(opens[0]!.parallelWith.map((ref) => ref.agentName)).toEqual(["trace-lane"])
- expect(opens[1]!.parallelWith.map((ref) => ref.agentName)).toEqual(["db-lane"])
- // The links point at the other lane's own row key.
- expect(opens[0]!.parallelWith[0]!.key).toBe(opens[1]!.key)
const marker = findRow(rows, "parallel")
- expect(marker.forkedBy).toBe("planner-agent")
- expect(marker.lanes).toHaveLength(2)
+ expect(marker.lanes.map((ref) => ref.agentName)).toEqual(["db-lane", "trace-lane"])
// The fork runs forwards, and both lanes really were open together.
expect(marker.startMs).toBeLessThan(marker.endMs)
expect(marker.overlapStartMs).toBe(T0 + 2 * SECOND)
@@ -1108,7 +1103,7 @@ describe("buildTranscript — parallel clustering", () => {
}),
]
- it("keeps a staggered run in one cluster and links only the pairs that overlapped", () => {
+ it("keeps a staggered run in one cluster", () => {
const rows = transcript(chain)
const marker = findRow(rows, "parallel")
expect(marker.lanes.map((lane) => lane.agentName)).toEqual(["a-lane", "b-lane", "c-lane"])
@@ -1119,13 +1114,6 @@ describe("buildTranscript — parallel clustering", () => {
// than reporting one that runs backwards.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()
-
- const opens = findRows(rows, "lane-open")
- expect(opens.map((row) => row.parallelWith.map((ref) => ref.agentName))).toEqual([
- ["b-lane"],
- ["a-lane", "c-lane"],
- ["b-lane"],
- ])
})
// The lane that breaks the run under a "previous member" rule: it ends before
@@ -1159,15 +1147,8 @@ describe("buildTranscript — parallel clustering", () => {
const rows = transcript(nested)
expect(findRows(rows, "parallel")).toHaveLength(1)
const marker = findRow(rows, "parallel")
- expect(marker.lanes).toHaveLength(3)
+ expect(marker.lanes.map((lane) => lane.agentName)).toEqual(["long-lane", "x-lane", "y-lane"])
expect(marker.startMs).toBeLessThan(marker.endMs)
-
- const opens = findRows(rows, "lane-open")
- expect(opens.map((row) => row.parallelWith.map((ref) => ref.agentName))).toEqual([
- ["x-lane", "y-lane"],
- ["long-lane"],
- ["long-lane"],
- ])
})
})
@@ -1689,39 +1670,25 @@ describe("buildTranscript — parallel turns", () => {
for (const row of findRows(rows, "assistant")) expect(row.depth).toBe(0)
})
- it("gives each member turn header reciprocal jump data", () => {
- const rows = transcript(twoTurns)
- const headers = findRows(rows, "turn")
- expect(headers.map((row) => row.turn.agentName)).toEqual(["log-lane", "metric-lane"])
- expect(headers[0]!.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["metric-lane"])
- expect(headers[1]!.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["log-lane"])
- // The links point at the other turn's own header row key.
- expect(headers[0]!.parallelWith[0]!.key).toBe(headers[1]!.key)
- expect(headers[1]!.parallelWith[0]!.key).toBe(headers[0]!.key)
- })
-
- // The marker's jump targets have to be row keys, not turn-shaped lookalikes:
- // a key that resolves to nothing scrolls nowhere and reports no error.
- it("resolves every jump key to a row that is on the page", () => {
+ // The marker counts its members, so a member it names has to be a turn that
+ // is really on the page — a phantom would be counted into the headline.
+ it("names every member turn with the key its header renders under", () => {
const rows = transcript(twoTurns)
const keys = new Set(rows.map((row) => row.key))
const marker = findRow(rows, "parallel-turns")
+ expect(marker.turns.map((ref) => ref.turn.agentName)).toEqual(["log-lane", "metric-lane"])
for (const ref of marker.turns) expect(keys.has(ref.key)).toBe(true)
- for (const header of findRows(rows, "turn")) {
- for (const ref of header.parallelWith) expect(keys.has(ref.key)).toBe(true)
- }
})
- it("keeps the marker and the chip data on a collapsed member", () => {
+ it("keeps the marker over a collapsed member", () => {
const rows = transcript(twoTurns, { collapsedTurns: new Set(["conversation:conv-a"]) })
// Collapse hides a turn's body, never its header — so the chapter-level
// concurrency survives it.
expect(kinds(rows)).toEqual(["parallel-turns", "turn", "turn", "assistant"])
- const collapsed = findRows(rows, "turn")[0]!
- expect(collapsed.parallelWith.map((ref) => ref.turn.agentName)).toEqual(["metric-lane"])
+ expect(findRows(rows, "turn")[0]!.depth).toBe(1)
})
- it("keeps a staggered run in one cluster and links only the pairs that overlapped", () => {
+ it("keeps a staggered run in one cluster", () => {
const staggered = [
...conversationTurn({ id: "a", agentName: "a-lane", startMs: 0, durationMs: 10 * SECOND }),
...conversationTurn({
@@ -1748,10 +1715,8 @@ describe("buildTranscript — parallel turns", () => {
// than reporting one that runs backwards.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()
-
- expect(
- findRows(rows, "turn").map((row) => row.parallelWith.map((ref) => ref.turn.agentName)),
- ).toEqual([["b-lane"], ["a-lane", "c-lane"], ["b-lane"]])
+ // Every member of the chain hangs off the one marker.
+ for (const header of findRows(rows, "turn")) expect(header.depth).toBe(1)
})
// The turn that breaks the run under a "previous member" rule: it ends before
@@ -1781,10 +1746,7 @@ describe("buildTranscript — parallel turns", () => {
// The two short turns never met each other, so only the long one is shared.
expect(marker.overlapStartMs).toBeUndefined()
expect(marker.overlapEndMs).toBeUndefined()
-
- expect(
- findRows(rows, "turn").map((row) => row.parallelWith.map((ref) => ref.turn.agentName)),
- ).toEqual([["x-lane", "y-lane"], ["long-lane"], ["long-lane"]])
+ for (const header of findRows(rows, "turn")) expect(header.depth).toBe(1)
})
it("says nothing about turns that ran one after the other", () => {
@@ -1799,7 +1761,7 @@ describe("buildTranscript — parallel turns", () => {
]
const rows = transcript(sequential)
expect(kinds(rows)).not.toContain("parallel-turns")
- for (const header of findRows(rows, "turn")) expect(header.parallelWith).toStrictEqual([])
+ for (const header of findRows(rows, "turn")) expect(header.depth).toBe(0)
})
// An `empty-turn` stub is HTTP/DB work with no agent activity in it. Pairing
@@ -1819,15 +1781,15 @@ describe("buildTranscript — parallel turns", () => {
]
const rows = transcript(spans)
expect(kinds(rows)).toEqual(["turn", "assistant", "empty-turn"])
- expect(findRow(rows, "turn").parallelWith).toStrictEqual([])
+ expect(findRow(rows, "turn").depth).toBe(0)
})
// Structural chrome, like the lane markers: the filtered view no longer has
// the ordering the marker describes.
- it("drops the marker and the chips while a query is active", () => {
+ it("drops the marker and its indentation while a query is active", () => {
const rows = transcript(twoTurns, { query: "metric-lane" })
expect(kinds(rows)).toEqual(["turn", "assistant"])
- expect(findRow(rows, "turn").parallelWith).toStrictEqual([])
+ expect(findRow(rows, "turn").depth).toBe(0)
})
it("marks the lab fixture's dispatched fan-out", () => {
diff --git a/apps/web/src/lib/agent-sessions/session-transcript.ts b/apps/web/src/lib/agent-sessions/session-transcript.ts
index 9b3a179ea..cb786995d 100644
--- a/apps/web/src/lib/agent-sessions/session-transcript.ts
+++ b/apps/web/src/lib/agent-sessions/session-transcript.ts
@@ -114,8 +114,6 @@ export type TranscriptRow =
/** AI spans the turn actually renders — deduped, app spans excluded. */
readonly aiSpanCount: number
readonly toolNames: readonly string[]
- /** Sibling turns this one genuinely overlapped — see `markParallelTurns`. */
- readonly parallelWith: readonly TranscriptTurnRef[]
})
/** A turn the transcript has nothing to say about, holding its ordinal open. */
| (RowBase & { readonly kind: "empty-turn"; readonly turn: SessionTurn })
@@ -168,7 +166,6 @@ export type TranscriptRow =
readonly spanCount: number
/** What the delegating tool call asked for, where a lane was delegated. */
readonly args: TranscriptPayload | undefined
- readonly parallelWith: readonly TranscriptLaneRef[]
})
| (RowBase & {
readonly kind: "lane-close"
@@ -183,7 +180,6 @@ export type TranscriptRow =
})
| (RowBase & {
readonly kind: "parallel"
- readonly forkedBy: string | undefined
/** The fork: from the first lane opening to the last one closing. */
readonly startMs: number
readonly endMs: number
@@ -272,7 +268,7 @@ export function buildTranscript(input: TranscriptInput): readonly TranscriptRow[
const turnRows = input.turns.map((turn) => buildTurn(turn, input, categoryOf))
// Structural chrome, like the lane markers: a filtered view no longer has the
// ordering the marker describes, so it is not built at all.
- const concurrent = filtering ? new Map() : markParallelTurns(turnRows)
+ const concurrent = filtering ? new Map() : markParallelTurns(turnRows)
// Capture coverage is a fact about the session, so it is counted over every
// turn before the first row is emitted.
@@ -299,15 +295,14 @@ export function buildTranscript(input: TranscriptInput): readonly TranscriptRow[
// a header over nothing is worse than no header. Collapse does not change
// that: the filter judges both states by the same rows.
if (filtering && entry.rows.length === 0) continue
- const marking = concurrent.get(entry.turn.id)
// The marker opens the cluster, so it is carried by its first member.
- if (marking?.marker !== undefined) body.push(marking.marker)
+ const marker = concurrent.get(entry.turn.id)
+ if (marker !== undefined) body.push(marker)
// A cluster member's whole chapter shifts one lane right, the same move a
// lane makes inside a turn: the indentation is what says "these chapters
// hang off the fork above" without the reader having to parse the marker.
- const indent = marking === undefined ? 0 : 1
- const header = marking?.header ?? entry.header
- body.push(indent === 0 ? header : { ...header, depth: header.depth + indent })
+ const indent = concurrent.has(entry.turn.id) ? 1 : 0
+ body.push(indent === 0 ? entry.header : { ...entry.header, depth: entry.header.depth + indent })
// Per-turn only where the session-level banner is not already up.
if (
!bannerUp &&
@@ -400,9 +395,6 @@ function buildTurn(
toolNames: distinct(
toolSpans.map((span) => span.genAi.toolName).filter((name): name is string => name !== undefined),
),
- // Filled in by `markParallelTurns`, which needs every turn to be built
- // before it can tell which of them overlapped.
- parallelWith: [],
}
// A collapsed turn renders its header and nothing else, so none of the work
@@ -731,7 +723,6 @@ function openedLane(
parentAgentName: scope.agentName,
spanCount: walk.counts.spans,
args: payload(delegating === undefined ? undefined : toolArgsText(delegating)),
- parallelWith: [],
},
...walk.rows,
{
@@ -789,21 +780,8 @@ function markParallelLanes(
const out = [...rows]
const markers: { at: number; row: TranscriptRow }[] = []
for (const cluster of clusters) {
- for (const lane of cluster.members) {
- // `at` is where the lane's rows were pushed and its opening row is the
- // first of them — a search by key would find the same row more slowly.
- const row = out[lane.at]
- if (row?.kind !== "lane-open") continue
- // Only the lanes this one GENUINELY overlapped: a cluster can be a chain
- // (A with B, B with C, A never with C), and linking A to C would be the
- // same invention the marker exists to prevent.
- out[lane.at] = {
- ...row,
- parallelWith: cluster.members
- .filter((other) => overlaps(lane, other))
- .map((other) => other.ref),
- }
- }
+ // `at` is where the lane's rows were pushed and its opening row is the
+ // first of them — the marker opens the cluster right above it.
const first = cluster.members[0]!
markers.push({
at: first.at,
@@ -811,7 +789,6 @@ function markParallelLanes(
kind: "parallel",
key: `${first.ref.key}:parallel`,
depth: scope.depth,
- forkedBy: scope.agentName,
startMs: cluster.startMs,
endMs: cluster.endMs,
overlapStartMs: cluster.overlapStartMs,
@@ -896,20 +873,6 @@ function clusterByOverlap(items: readonly T[]): readonly Ove
})
}
-/** Two intervals open at the same moment. Identity, not keys: the members are
- * the very objects handed to `clusterByOverlap`. */
-function overlaps(a: T, b: T): boolean {
- return a !== b && a.startMs < b.endMs && b.startMs < a.endMs
-}
-
-/** What a cluster does to one of its member turns. */
-interface TurnMarking {
- /** The header again, carrying the turns this one overlapped. */
- readonly header: Extract
- /** The cluster's marker, on its first member only. */
- readonly marker: TranscriptRow | undefined
-}
-
/**
* Chapter-level parallelism: turns that ran at the same time.
*
@@ -924,7 +887,7 @@ interface TurnMarking {
* a placeholder for HTTP/DB work with no agent activity in it, and pairing one
* with a real turn would announce a concurrency the reader cannot see.
*/
-function markParallelTurns(entries: readonly TurnRows[]): ReadonlyMap {
+function markParallelTurns(entries: readonly TurnRows[]): ReadonlyMap {
const items = entries
.filter((entry) => entry.aiSpanCount > 0)
.map((entry, order) => ({
@@ -938,34 +901,20 @@ function markParallelTurns(entries: readonly TurnRows[]): ReadonlyMap()
+ // Membership is the key; only the cluster's first member carries its marker.
+ const marked = new Map()
for (const cluster of clusterByOverlap(items)) {
- for (const item of cluster.members) {
- marked.set(item.entry.turn.id, {
- header: {
- ...item.entry.header,
- // Only the turns this one GENUINELY overlapped: a cluster can be a
- // chain (A with B, B with C, A never with C).
- parallelWith: cluster.members
- .filter((other) => overlaps(item, other))
- .map((other) => other.ref),
- },
- marker: undefined,
- })
- }
+ for (const item of cluster.members) marked.set(item.entry.turn.id, undefined)
const first = cluster.members.reduce((a, b) => (a.order <= b.order ? a : b))
marked.set(first.entry.turn.id, {
- ...marked.get(first.entry.turn.id)!,
- marker: {
- kind: "parallel-turns",
- key: `${first.ref.key}:parallel-turns`,
- depth: 0,
- startMs: cluster.startMs,
- endMs: cluster.endMs,
- overlapStartMs: cluster.overlapStartMs,
- overlapEndMs: cluster.overlapEndMs,
- turns: cluster.members.map((item) => item.ref),
- },
+ kind: "parallel-turns",
+ key: `${first.ref.key}:parallel-turns`,
+ depth: 0,
+ startMs: cluster.startMs,
+ endMs: cluster.endMs,
+ overlapStartMs: cluster.overlapStartMs,
+ overlapEndMs: cluster.overlapEndMs,
+ turns: cluster.members.map((item) => item.ref),
})
}
return marked