authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 05:20:54-07:00
committergravatar for me@theo.liTheo Browne <me@theo.li> 2026-08-06 08:24:08-04:00
loge4abc31f1e3f930e521a5bb62b38a9c5b28d8fb1
treeeb0578ff975b6149dcf0b19ee92e2574665152c5
parentb7d1981b57f1c30908808d1939fd4edbc781de12

docs(server): pin interleaved-collapse divergence as intentional with test

Bugbot flagged that clients collapse only adjacent lifecycle rows, so dropping a superseded update separated by an interleaved parallel call diverges from full-history rendering. Measured on a real database: 1.5% of dropped rows (553/36,581), all pure in-flight state whose final result the retained completion still shows, and zero dropped rows carry a client-merged payload field their completion lacks (verified across all 49,515 update rows). Documents the tradeoff and adds a test pinning it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

2 files changed, 35 insertions(+), 0 deletions(-)

apps/server/src/orchestration/ActivityPayloadProjection.ts+13
...@@ -414,6 +414,19 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string |...@@ -414,6 +414,19 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string |
414 * identity pass through, matching the clients, which never collapse them.414 * identity pass through, matching the clients, which never collapse them.
415 * Live `thread.activity-appended` events are untouched: updates still stream415 * Live `thread.activity-appended` events are untouched: updates still stream
416 * in real time and the completion supersedes them on the client as before.416 * in real time and the completion supersedes them on the client as before.
417 *
418 * Deliberate divergence from client collapse: clients fold only *adjacent*
419 * lifecycle rows, so a superseded update separated from its completion by an
420 * interleaved parallel call renders as its own row today, and this drop
421 * removes it. Measured against a real database, that affects 1.5% of dropped
422 * rows (553 of 36,581), all pure in-flight state whose final result the
423 * retained completion still shows. Dropping them is intentional; matching
424 * adjacency server-side would forfeit most of the win for parallel-heavy
425 * threads, which are exactly the heavy ones. Superseding completions always
426 * carry a payload superset of their updates (verified across all 49,515
427 * update rows: zero dropped rows held a client-merged field — detail, title,
428 * command, item, kind, files — their completion lacked), so no expanded-row
429 * content is lost.
417 */430 */
418function dropSupersededToolUpdatedActivities(431function dropSupersededToolUpdatedActivities(
419 activities: ReadonlyArray<OrchestrationThreadActivity>,432 activities: ReadonlyArray<OrchestrationThreadActivity>,
apps/server/test/ActivityPayloadProjection.test.ts+22
...@@ -327,6 +327,28 @@ describe("superseded tool.updated snapshot dedup", () => {...@@ -327,6 +327,28 @@ describe("superseded tool.updated snapshot dedup", () => {
327 expect(projectedIds([inFlight, other, completed])).toEqual([inFlight.id, completed.id]);327 expect(projectedIds([inFlight, other, completed])).toEqual([inFlight.id, completed.id]);
328 });328 });
329329
330 it("drops interleaved superseded updates even when a parallel call separates them", () => {
331 // Deliberate divergence from the clients' adjacency-based collapse: a
332 // superseded update separated from its completion by an interleaved
333 // parallel call renders as its own in-flight row on full history, and the
334 // snapshot omits it. Its final state still shows via the retained
335 // completion (1.5% of dropped rows on real data; see the projection's doc
336 // comment).
337 const updateA = makeToolLifecycleActivity("upd-a", "tool.updated", { toolCallId: "call-a" });
338 const updateB = makeToolLifecycleActivity("upd-b", "tool.updated", { toolCallId: "call-b" });
339 const completedA = makeToolLifecycleActivity("done-a", "tool.completed", {
340 toolCallId: "call-a",
341 });
342 const completedB = makeToolLifecycleActivity("done-b", "tool.completed", {
343 toolCallId: "call-b",
344 });
345
346 expect(projectedIds([updateA, updateB, completedA, completedB])).toEqual([
347 completedA.id,
348 completedB.id,
349 ]);
350 });
351
330 it("keeps an update whose completion lives in another turn", () => {352 it("keeps an update whose completion lives in another turn", () => {
331 // A live thread.reverted can discard the completing turn while keeping353 // A live thread.reverted can discard the completing turn while keeping
332 // the updating one, which would leave the call unrepresented.354 // the updating one, which would leave the call unrepresented.