Skip to content

fix(web): parallel turns — announce overlap, assign spans by conversation id - #632

Merged
JeremyFunk merged 2 commits into
mainfrom
feat/agent-session-transcript-parallel-turns
Aug 25, 2026
Merged

fix(web): parallel turns — announce overlap, assign spans by conversation id#632
JeremyFunk merged 2 commits into
mainfrom
feat/agent-session-transcript-parallel-turns

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Two related fixes for concurrent agent work that lands as sibling turns rather than lanes inside one turn — the shape Maple's own investigation fan-out produces, since every pass stamps its own turn id.

Announce turns that ran in parallel

The transcript's parallelism treatment stopped at lane level: overlapping lanes inside one turn got a fork marker and jump chips, but overlapping turn chapters rendered as silently sequential. Turn-level overlap now gets the same treatment — the lane clustering (running max-end, honest overlap window only when a shared window exists) is lifted into a generic clusterByOverlap used by both levels. A parallel-turns marker row precedes the cluster ("3 turns ran at the same time — they overlap 12:31:51 → 12:32:02. Each turn is shown whole, in order: …") and each member's header carries reciprocal "ran in parallel with TURN n" jump chips, collapsed or not. Markers stay structural chrome: dropped under an active filter, excluded for no-AI chapter stubs.

Assign spans to conversation turns by the id they carry

buildSessionTurns' conversation-id partition assigned spans by a pure time cursor, so N concurrent turn ids interleaved: every lane's tail spans — its final answer, its submit tool call — landed in whichever lane started last, and the transcript rendered them unattributed in the wrong chapter (its parentage re-threading is per-turn and could not repair a cross-turn split).

The conversation partition now assigns each span to the turn whose id it carries; untagged spans (the app's own HTTP/DB children) follow parentSpanId to the nearest tagged ancestor, and only spans with no link to any turn fall back to the time cursor. The session-id-derived exclusion, the agent-root and trace partitions, and pre-first-anchor behavior are all unchanged, and turn bounds/traces/failure/label still derive from the member sets. Same-millisecond anchors no longer drop the earlier turn on this path — an anchor carries its own id.

The two fixes compose: once each lane owns its spans, the fan-out reads as correctly-attributed sibling chapters, and the marker is what says they ran together.

Tests: +20 (fan-out regression proving each lane keeps its tail spans — fails on the old assignment; transitive/staggered/nested turn clustering with honest windows; the end-to-end fan-out → sibling chapters → marker combination; within-turn lane clustering anti-regression). 270 scoped tests and web typecheck green.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

JeremyFunk added 2 commits August 25, 2026 16:45
The transcript marked concurrency only WITHIN a turn: overlapping
agent-level lanes got a marker row and reciprocal jump chips. But turn
partitioning gives each Maple sub-agent or dispatched run its own turn
— its own conversation id, or its own trace once a queue hop drops the
span link — so a fan-out arrives as sibling chapters and read as
"TURN 3, then TURN 4": the one thing the timestamps deny.

Lifts the lane clustering into `clusterByOverlap`, generic over
{ startMs, endMs }, and uses it for both levels. Same honesty rules
either way: a running max-end so two short runs nested in one long one
stay one cluster, an overlap window reported only where every member
genuinely shared one, and `parallelWith` listing only the pairs that
actually met — a chain (A–B, B–C) never links A to C.

Turns with no AI activity sit it out: an `empty-turn` stub is HTTP/DB
work, and pairing one with a real turn would claim a concurrency the
reader cannot see. Markers and chips are structural chrome, so a query
drops them exactly as it drops the lane markers.

The lab fixture grows a dispatched fan-out — `release-triage` queues
two checks and waits — which lands as three overlapping chapters.
Deliberately keyed on agent roots rather than conversation ids:
`findAnchors` prefers conversation ids for the WHOLE session as soon as
two exist, so stamping them would repartition the other seventeen turns.
Maple's investigation fan-out stamps a `maple_ai.turn.id` per hypothesis
lane and launches the lanes with `Promise.all`, so the conversation ids
that partition the session INTERLEAVE. Span assignment was a pure time
cursor — a turn owned everything that started before the next anchor —
and concurrent anchors start milliseconds apart, so each lane kept only
a sliver and the whole concurrent phase, every lane's final chat and its
`execute_tool submit_candidate`, landed in whichever lane anchored last.
The transcript re-threads by parentage only within a turn, so it could
not repair it: the tail spans' parent lived in another turn's bucket,
they became forest roots and rendered at depth 0, unattributed, under a
chapter that never ran them.

On the conversation rule the id a span CARRIES now decides, keeping the
existing exclusion of ids equal to the session id. Untagged spans — the
app's own HTTP and database work sharing the agent's trace — follow
parentage to their nearest tagged ancestor, which is cheap and beats the
cursor exactly where the cursor was wrong; `parentSpanId` is intra-trace,
so a span with no tagged ancestor still falls back to the time cursor.
The agent-root and trace rules are untouched, and turn start/end/traces/
failed/label keep deriving from the member sets.

This also fixes an adjacent case: two conversation anchors sharing a
millisecond used to leave the earlier bucket empty and drop its turn.
An anchor carries its own id, so it keeps its turn.

With the lanes now owning their spans they are sibling turns, which is
what the chapter-level parallel markers were built for — a transcript
test pins the combination end to end.
@JeremyFunk
JeremyFunk merged commit 184ba09 into main Aug 25, 2026
32 checks passed
@JeremyFunk
JeremyFunk deleted the feat/agent-session-transcript-parallel-turns branch August 25, 2026 14:50
@github-actions

Copy link
Copy Markdown

🍁 Maple PR preview

Warning

Preview cleanup could not be confirmed. The Alchemy teardown outcome was skipped.

Final commit e08bda2 · View workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant