fix(sync): treat part snapshot as a delta coalescing barrier (#1693)

A `message.part.updated` snapshot did not invalidate the pending delta
coalescing key for its message/part. A delta arriving after an intervening
snapshot merged into a delta queued before it, and the snapshot then
overwrote that slot, dropping the later delta's text (e.g. `abc` rendered
as `ab`). Enqueueing a part snapshot now drops that part's pending delta
coalescing keys, while leaving already-queued delta events in place, so
post-snapshot deltas start a fresh entry.

Closes #1647.

Co-authored-by: Ibrahim Khan <ibrakhxn@amazon.com>
This commit is contained in:
Ibrahim Khan
2026-06-18 01:09:24 +03:00
committed by GitHub
co-authored by Ibrahim Khan
parent 08a851f902
commit fbc108f6ba
2 changed files with 65 additions and 0 deletions
@@ -97,6 +97,51 @@ describe("createEventPipeline", () => {
})).toEqual(["updated:a", "delta:b", "updated:ab"])
})
test("does not merge deltas across an intervening part snapshot", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: () => void
const deliveredAll = new Promise<void>((resolve) => {
resolveDelivered = resolve
})
const delivered: Event[] = []
const pipeline = createEventPipeline({
sdk: createSdk([
partUpdatedEvent("a"),
deltaEvent("b"),
partUpdatedEvent("ab"),
deltaEvent("c"),
], resolveStreamFinished),
onEvent: (_directory, payload) => {
delivered.push(payload)
if (delivered.length === 4) {
resolveDelivered()
}
},
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
await Promise.race([deliveredAll, new Promise<void>((resolve) => setTimeout(resolve, 300))])
} finally {
pipeline.cleanup()
}
// The "ab" snapshot is a coalescing barrier: the trailing "c" delta must
// stay a separate event after it, not merge into the "b" delta queued
// before the snapshot (which the snapshot would then overwrite).
expect(delivered.map((event) => {
if (event.type === "message.part.delta") {
return `delta:${(event.properties as { delta: string }).delta}`
}
return `updated:${((event.properties as { part: { text: string } }).part).text}`
})).toEqual(["updated:a", "delta:b", "updated:ab", "delta:c"])
})
test("normalizes openchamber session status events", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {