import { EventId, MessageId, ThreadId, TurnId, OrchestrationEvent, type OrchestrationThreadActivity,} from "@t3tools/contracts";import { it } from "@effect/vitest";import * as Clock from "effect/Clock";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import { describe, expect } from "vite-plus/test"; import { coalesceLiveToolUpdatedEvents, makeThreadLiveEventCoalescer,} from "./ThreadLiveEventCoalescer.ts"; const threadId = ThreadId.make("thread-coalescer-test");const turnId = TurnId.make("turn-coalescer-test");const encodeEvent = Schema.encodeSync(Schema.fromJsonString(OrchestrationEvent)); function makeToolActivity( sequence: number, options: { readonly kind?: "tool.updated" | "tool.completed"; readonly toolCallId?: string; readonly turnId?: TurnId; } = {},): OrchestrationEvent { const { kind = "tool.updated", toolCallId = "call-edit", turnId: activityTurnId = turnId, } = options; const activity: OrchestrationThreadActivity = { id: EventId.make(`activity-${sequence}`), tone: "tool", kind, summary: "Editing app.ts", payload: { itemType: "file_change", title: "Editing app.ts", data: toolCallId ? { toolCallId } : {}, }, turnId: activityTurnId, createdAt: "2026-01-01T00:00:01.000Z", }; return { sequence, eventId: EventId.make(`event-${sequence}`), aggregateKind: "thread", aggregateId: threadId, occurredAt: "2026-01-01T00:00:01.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "thread.activity-appended", payload: { threadId, activity }, };} function makeMessage(sequence: number, text = "Still working"): OrchestrationEvent { return { sequence, eventId: EventId.make(`event-${sequence}`), aggregateKind: "thread", aggregateId: threadId, occurredAt: "2026-01-01T00:00:02.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "thread.message-sent", payload: { threadId, messageId: MessageId.make(`message-${sequence}`), role: "assistant", text, turnId, streaming: false, createdAt: "2026-01-01T00:00:02.000Z", updatedAt: "2026-01-01T00:00:02.000Z", }, };} describe("ThreadLiveEventCoalescer", () => { it("coalesces only calls with a stable toolCallId", () => { const events = [ makeToolActivity(1, { toolCallId: "call-a" }), makeToolActivity(2, { toolCallId: "call-b" }), makeToolActivity(3, { toolCallId: "call-a" }), ]; expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([2, 3]); }); it("preserves parallel same-label calls without a stable toolCallId", () => { const events = [ makeToolActivity(1, { toolCallId: "" }), makeToolActivity(2, { toolCallId: "" }), makeToolActivity(3, { kind: "tool.completed", toolCallId: "" }), ]; expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([1, 2, 3]); }); it("does not coalesce stable tool calls across turns", () => { const events = [ makeToolActivity(1, { turnId: TurnId.make("turn-old") }), makeToolActivity(2, { turnId: TurnId.make("turn-new") }), ]; expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([1, 2]); }); it("flushes a stable update run before a completion boundary", () => { const events = [ makeToolActivity(1), makeToolActivity(2), makeToolActivity(3, { kind: "tool.completed" }), makeToolActivity(4), ]; expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([2, 3, 4]); }); it.effect("flushes pending tool updates as soon as an unrelated event arrives", () => Effect.scoped( Effect.gen(function* () { const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "500 millis" }); const startedAt = yield* Clock.currentTimeMillis; yield* Effect.forEach( Array.from({ length: 10 }, (_, index) => index + 2), (sequence) => coalescer.offer({ kind: "event", event: makeToolActivity(sequence) }), { discard: true }, ); yield* coalescer.offer({ kind: "event", event: makeMessage(12) }); expect(yield* Clock.currentTimeMillis).toBe(startedAt); expect( (yield* coalescer.stream.pipe(Stream.take(2), Stream.runCollect)).map((item) => item.kind === "event" ? item.event.sequence : item.kind, ), ).toEqual([11, 12]); }), ).pipe(Effect.provide(TestClock.layer())), ); it.effect("flushes pending tool updates as soon as a synchronization marker arrives", () => Effect.scoped( Effect.gen(function* () { const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "500 millis" }); const startedAt = yield* Clock.currentTimeMillis; yield* coalescer.offer({ kind: "event", event: makeToolActivity(2) }); yield* coalescer.offer({ kind: "event", event: makeToolActivity(3) }); yield* coalescer.offer({ kind: "synchronized" }); expect(yield* Clock.currentTimeMillis).toBe(startedAt); expect( (yield* coalescer.stream.pipe(Stream.take(2), Stream.runCollect)).map((item) => item.kind === "event" ? item.event.sequence : item.kind, ), ).toEqual([3, "synchronized"]); }), ).pipe(Effect.provide(TestClock.layer())), ); it.effect("fails and clears pending updates when their serialized payload fills the budget", () => Effect.scoped( Effect.gen(function* () { const first = makeToolActivity(1); const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "500 millis", maxSerializedBytes: Buffer.byteLength(encodeEvent(first)), }); yield* coalescer.offer({ kind: "event", event: first }); expect(yield* coalescer.usage).toEqual({ retainedItems: 1, retainedSerializedBytes: Buffer.byteLength(encodeEvent(first)), }); const overflow = yield* coalescer .offer({ kind: "event", event: makeToolActivity(2) }) .pipe(Effect.result); expect(overflow._tag).toBe("Failure"); yield* coalescer.closed; expect(yield* coalescer.usage).toEqual({ retainedItems: 0, retainedSerializedBytes: 0 }); const marker = yield* coalescer.offer({ kind: "synchronized" }).pipe(Effect.result); expect(marker._tag).toBe("Failure"); const delivered = yield* coalescer.stream.pipe(Stream.runCollect, Effect.result); expect(delivered._tag).toBe("Failure"); }), ), ); it.effect("keeps the flush timer alive when an offer's shorter scope closes", () => Effect.scoped( Effect.gen(function* () { const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "50 millis" }); yield* Effect.scoped(coalescer.offer({ kind: "event", event: makeToolActivity(1) })); yield* TestClock.adjust("50 millis"); const items = yield* coalescer.stream.pipe(Stream.take(1), Stream.runCollect); expect( items.map((item) => (item.kind === "event" ? item.event.sequence : item.kind)), ).toEqual([1]); }), ), ); it.effect("keeps an unacknowledged batch charged and clears later events on overflow", () => Effect.scoped( Effect.gen(function* () { const coalescer = yield* makeThreadLiveEventCoalescer({ maxItems: 3 }); const first = makeMessage(1, "é".repeat(1_024)); yield* coalescer.offer({ kind: "event", event: first }); yield* Effect.scoped( Effect.gen(function* () { const pull = yield* Stream.toPull(coalescer.stream); const batch = yield* pull; expect( batch.map((item) => (item.kind === "event" ? item.event.sequence : null)), ).toEqual([1]); yield* coalescer.offer({ kind: "event", event: makeMessage(2) }); yield* coalescer.offer({ kind: "event", event: makeToolActivity(3) }); expect((yield* coalescer.usage).retainedItems).toBe(3); const overflow = yield* coalescer .offer({ kind: "event", event: makeToolActivity(4, { kind: "tool.completed" }) }) .pipe(Effect.result); expect(overflow._tag).toBe("Failure"); // Do not pull or acknowledge the batch. Cleanup must still finish. yield* coalescer.closed; expect(yield* coalescer.usage).toEqual({ retainedItems: 1, retainedSerializedBytes: Buffer.byteLength(encodeEvent(first)), }); }), ); expect(yield* coalescer.usage).toEqual({ retainedItems: 0, retainedSerializedBytes: 0 }); }), ), );});
import {
EventId,
MessageId,
ThreadId,
TurnId,
OrchestrationEvent,
type OrchestrationThreadActivity,
} from "@t3tools/contracts";
import { it } from "@effect/vitest";
import * as Clock from "effect/Clock";
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";
import { describe, expect } from "vite-plus/test";
import {
coalesceLiveToolUpdatedEvents,
makeThreadLiveEventCoalescer,
} from "./ThreadLiveEventCoalescer.ts";
const threadId = ThreadId.make("thread-coalescer-test");
const turnId = TurnId.make("turn-coalescer-test");
const encodeEvent = Schema.encodeSync(Schema.fromJsonString(OrchestrationEvent));
function makeToolActivity(
sequence: number,
options: {
readonly kind?: "tool.updated" | "tool.completed";
readonly toolCallId?: string;
readonly turnId?: TurnId;
} = {},
): OrchestrationEvent {
const {
kind = "tool.updated",
toolCallId = "call-edit",
turnId: activityTurnId = turnId,
} = options;
const activity: OrchestrationThreadActivity = {
id: EventId.make(`activity-${sequence}`),
tone: "tool",
kind,
summary: "Editing app.ts",
payload: {
itemType: "file_change",
title: "Editing app.ts",
data: toolCallId ? { toolCallId } : {},
},
turnId: activityTurnId,
createdAt: "2026-01-01T00:00:01.000Z",
};
return {
sequence,
eventId: EventId.make(`event-${sequence}`),
aggregateKind: "thread",
aggregateId: threadId,
occurredAt: "2026-01-01T00:00:01.000Z",
commandId: null,
causationEventId: null,
correlationId: null,
metadata: {},
type: "thread.activity-appended",
payload: { threadId, activity },
};
}
function makeMessage(sequence: number, text = "Still working"): OrchestrationEvent {
return {
sequence,
eventId: EventId.make(`event-${sequence}`),
aggregateKind: "thread",
aggregateId: threadId,
occurredAt: "2026-01-01T00:00:02.000Z",
commandId: null,
causationEventId: null,
correlationId: null,
metadata: {},
type: "thread.message-sent",
payload: {
threadId,
messageId: MessageId.make(`message-${sequence}`),
role: "assistant",
text,
turnId,
streaming: false,
createdAt: "2026-01-01T00:00:02.000Z",
updatedAt: "2026-01-01T00:00:02.000Z",
},
};
}
describe("ThreadLiveEventCoalescer", () => {
it("coalesces only calls with a stable toolCallId", () => {
const events = [
makeToolActivity(1, { toolCallId: "call-a" }),
makeToolActivity(2, { toolCallId: "call-b" }),
makeToolActivity(3, { toolCallId: "call-a" }),
];
expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([2, 3]);
});
it("preserves parallel same-label calls without a stable toolCallId", () => {
const events = [
makeToolActivity(1, { toolCallId: "" }),
makeToolActivity(2, { toolCallId: "" }),
makeToolActivity(3, { kind: "tool.completed", toolCallId: "" }),
];
expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([1, 2, 3]);
});
it("does not coalesce stable tool calls across turns", () => {
const events = [
makeToolActivity(1, { turnId: TurnId.make("turn-old") }),
makeToolActivity(2, { turnId: TurnId.make("turn-new") }),
];
expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([1, 2]);
});
it("flushes a stable update run before a completion boundary", () => {
const events = [
makeToolActivity(1),
makeToolActivity(2),
makeToolActivity(3, { kind: "tool.completed" }),
makeToolActivity(4),
];
expect(coalesceLiveToolUpdatedEvents(events).map((event) => event.sequence)).toEqual([2, 3, 4]);
});
it.effect("flushes pending tool updates as soon as an unrelated event arrives", () =>
Effect.scoped(
Effect.gen(function* () {
const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "500 millis" });
const startedAt = yield* Clock.currentTimeMillis;
yield* Effect.forEach(
Array.from({ length: 10 }, (_, index) => index + 2),
(sequence) => coalescer.offer({ kind: "event", event: makeToolActivity(sequence) }),
{ discard: true },
);
yield* coalescer.offer({ kind: "event", event: makeMessage(12) });
expect(yield* Clock.currentTimeMillis).toBe(startedAt);
expect(
(yield* coalescer.stream.pipe(Stream.take(2), Stream.runCollect)).map((item) =>
item.kind === "event" ? item.event.sequence : item.kind,
),
).toEqual([11, 12]);
}),
).pipe(Effect.provide(TestClock.layer())),
);
it.effect("flushes pending tool updates as soon as a synchronization marker arrives", () =>
Effect.scoped(
Effect.gen(function* () {
const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "500 millis" });
const startedAt = yield* Clock.currentTimeMillis;
yield* coalescer.offer({ kind: "event", event: makeToolActivity(2) });
yield* coalescer.offer({ kind: "event", event: makeToolActivity(3) });
yield* coalescer.offer({ kind: "synchronized" });
expect(yield* Clock.currentTimeMillis).toBe(startedAt);
expect(
(yield* coalescer.stream.pipe(Stream.take(2), Stream.runCollect)).map((item) =>
item.kind === "event" ? item.event.sequence : item.kind,
),
).toEqual([3, "synchronized"]);
}),
).pipe(Effect.provide(TestClock.layer())),
);
it.effect("fails and clears pending updates when their serialized payload fills the budget", () =>
Effect.scoped(
Effect.gen(function* () {
const first = makeToolActivity(1);
const coalescer = yield* makeThreadLiveEventCoalescer({
coalesceWindow: "500 millis",
maxSerializedBytes: Buffer.byteLength(encodeEvent(first)),
});
yield* coalescer.offer({ kind: "event", event: first });
expect(yield* coalescer.usage).toEqual({
retainedItems: 1,
retainedSerializedBytes: Buffer.byteLength(encodeEvent(first)),
});
const overflow = yield* coalescer
.offer({ kind: "event", event: makeToolActivity(2) })
.pipe(Effect.result);
expect(overflow._tag).toBe("Failure");
yield* coalescer.closed;
expect(yield* coalescer.usage).toEqual({ retainedItems: 0, retainedSerializedBytes: 0 });
const marker = yield* coalescer.offer({ kind: "synchronized" }).pipe(Effect.result);
expect(marker._tag).toBe("Failure");
const delivered = yield* coalescer.stream.pipe(Stream.runCollect, Effect.result);
expect(delivered._tag).toBe("Failure");
}),
),
);
it.effect("keeps the flush timer alive when an offer's shorter scope closes", () =>
Effect.scoped(
Effect.gen(function* () {
const coalescer = yield* makeThreadLiveEventCoalescer({ coalesceWindow: "50 millis" });
yield* Effect.scoped(coalescer.offer({ kind: "event", event: makeToolActivity(1) }));
yield* TestClock.adjust("50 millis");
const items = yield* coalescer.stream.pipe(Stream.take(1), Stream.runCollect);
expect(
items.map((item) => (item.kind === "event" ? item.event.sequence : item.kind)),
).toEqual([1]);
}),
),
);
it.effect("keeps an unacknowledged batch charged and clears later events on overflow", () =>
Effect.scoped(
Effect.gen(function* () {
const coalescer = yield* makeThreadLiveEventCoalescer({ maxItems: 3 });
const first = makeMessage(1, "é".repeat(1_024));
yield* coalescer.offer({ kind: "event", event: first });
yield* Effect.scoped(
Effect.gen(function* () {
const pull = yield* Stream.toPull(coalescer.stream);
const batch = yield* pull;
expect(
batch.map((item) => (item.kind === "event" ? item.event.sequence : null)),
).toEqual([1]);
yield* coalescer.offer({ kind: "event", event: makeMessage(2) });
yield* coalescer.offer({ kind: "event", event: makeToolActivity(3) });
expect((yield* coalescer.usage).retainedItems).toBe(3);
const overflow = yield* coalescer
.offer({ kind: "event", event: makeToolActivity(4, { kind: "tool.completed" }) })
.pipe(Effect.result);
expect(overflow._tag).toBe("Failure");
// Do not pull or acknowledge the batch. Cleanup must still finish.
yield* coalescer.closed;
expect(yield* coalescer.usage).toEqual({
retainedItems: 1,
retainedSerializedBytes: Buffer.byteLength(encodeEvent(first)),
});
}),
);
expect(yield* coalescer.usage).toEqual({ retainedItems: 0, retainedSerializedBytes: 0 });
}),
),
);
});