refactor(web,server): match a turn to its reply by execution id, and clamp text through one primitive#545
Merged
Merged
Conversation
…clamp text through one primitive Two follow-ups to #538/#539. `session.replied` carried no turn identity, so the client matched a fold to a reply by ordinal: the Nth reply-producing fold in the stream was assumed to be the Nth reply in the thread. That holds only while the two stay 1:1, and a failed execution posts no session.replied — so the clocks could drift and nest a turn's work under someone else's answer. The terminal execution_state frame that drives completeSession already knows which execution answered, so it now says so: session.replied carries execution_id, the reducer stamps each item with the execution that produced it, and folds carry it through. Placement matches on identity, and the ordinal machinery is gone. Deliberately no fallback: replies written before this change have no execution id and do not nest — their work keeps a standalone row. One code path, at the cost of nesting on existing threads. When a steer splits a running execution into several folds, the execution's answer is the last of them; that one nests and the earlier ones stay standalone. MessageText's own max-height, so "Show more" shrank the message. That was fixed with an opt-out prop — a convention, and conventions get forgotten. ClampedBlock carries the clamp in context, so an inner block declines to clamp and the nesting is unrepresentable rather than merely discouraged. It also measures overflow instead of trusting a length heuristic, so a message long enough to trip the threshold but short enough to fit no longer offers a toggle that does nothing. Adopted by the two sites that genuinely share the shape. EntryQuoteCard and SessionCapabilitiesPopover keep their own: the first collapses a changes list behind a domain action row, and the second slices an item array rather than clamping a box. Forcing them through the primitive would bend it out of shape to no benefit.
…it came from Making `executionId` required is what surfaced these: the workspace typecheck found two constructors the change had missed, in packages the lane's scope excluded. Tests didn't catch them because tests don't typecheck. The server's session-records projection replays stored events, which carry no execution boundary — unlike the live stream, where execution_state frames stamp each item — so null is the honest value there. Mobile's fixtures just had to say so; mobile does no fold-to-reply placement, so nothing in its logic moves.
GitHub dropped the pull_request synchronize event for b4e07dc — only the pull_request_target CLA workflow fired. Reopening the PR did not re-fire it either, so this nudges a fresh SHA.
gbasin
force-pushed
the
fix/turn-identity-and-clamp
branch
from
July 16, 2026 20:05
2c4eb4f to
ef12d6a
Compare
ToolCallItem gained a required executionId, but this hand-built literal was never updated. vitest does not typecheck, so only the build caught it.
This was referenced Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The two follow-ups worth doing from the #538/#539 thread, built in parallel lanes and reviewed here. The third (tokenizing WorkFold's
ml-[43px]) I dropped after investigating — see the bottom.1. Fold→reply matching by identity, not position
session.repliedcarried{session_id, text, broadcast}and no turn identity, sobuildSpineRowsmatched by ordinal: the Nth reply-producing fold in the SSE stream was assumed to be the Nthsession.repliedin the thread. True only while they stay 1:1 — and a failed execution posts nosession.replied, so the two clocks drift and a turn's work can nest under someone else's answer. (Prod thread 575 has exactly this shape: 5 executions, 4 replies. It doesn't currently mis-attribute only because the failed turn produced no fold.)The terminal
execution_stateframe that drivescompleteSessionalready knows which execution answered, so it now says so:session.repliedpayload carriesexecution_idSessionStatetracks the current execution; every item is stamped with the execution that produced it (following the existingArtifact.executionIdprecedent)FoldedTurnRow.executionIdChatMessage.sessionExecutionId, decoded-with-default so legacy events yieldnullrather than throwingNo fallback, deliberately (product call): replies written before this change have no execution id and simply don't nest — their work keeps a standalone row. One code path, paid for with nesting on existing threads. Where a steer splits a running execution into several folds, the execution's answer is the last one — that nests, earlier ones stay standalone. No fold is ever dropped.
2. One clamp primitive
#538's bug was two clamps nesting:
CompactReply'sline-clamp-3wrappedMessageText's ownmax-h-80, so "Show more" shrank the message (360px → 87px in a real browser). #538 fixed it with acollapsibleopt-out — a convention, and conventions get forgotten.ClampedBlockputs the clamp in React context, so an inner block declines to clamp: the nesting is now unrepresentable, not merely discouraged. It also measures overflow rather than trusting a length heuristic, so a message that trips the threshold but fits on screen no longer offers a toggle that does nothing (the one visible behaviour change).Adopted at 2 of 4 sites, on purpose.
EntryQuoteCardcollapses a changes list behind a domain-specific action row;SessionCapabilitiesPopoverslice()s an item array and has no overflowing box to measure. Forcing either through the primitive would bend it out of shape for no benefit — the duplication there is honest.Verification
cluster-steer-interleave(incl. "Show more grows a long agent reply" — jsdom has no layout and cannot judge it) andsession-pane-uxwork-fold specs all pass through the new primitive.masterwith zero changes fails 1–2 tests per full run (addressability-shell:81,nav-discoverability:23), a rotating cast that all pass in isolation. This branch shows the same rate and character. I nearly mis-attributed that to these changes.Dropped: the WorkFold gutter token
ml-[43px]/calc(100%-59px)appear exactly once in the codebase, already behind anestedprop with an explanatory comment. There's no duplication to remove, and the numbers don't align to anything I could name truthfully (MessageRow's content column computes to 60px, not 43). Naming a single-use constant would be indirection for its own sake.