| author | |
| committer | |
| log | e4abc31f1e3f930e521a5bb62b38a9c5b28d8fb1 |
| tree | eb0578ff975b6149dcf0b19ee92e2574665152c5 |
| parent | b7d1981b57f1c30908808d1939fd4edbc781de12 |
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 stream | 415 | * 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 | */ |
| 418 | function dropSupersededToolUpdatedActivities( | 431 | function 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 | }); |
| 329 | 329 | ||
| 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 keeping | 353 | // 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. |