Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions apps/server/src/orchestration/ActivityPayloadProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,8 @@ function toolLifecycleIdentity(activity: OrchestrationThreadActivity): string |
* update within the turn — a later update belongs to a subsequent call that
* reuses the same identity and is still in flight. Rows without a lifecycle
* identity pass through, matching the clients, which never collapse them.
* Live `thread.activity-appended` events are untouched: updates still stream
* in real time and the completion supersedes them on the client as before.
* Live `thread.activity-appended` events use the same identity in
* `coalesceLiveToolUpdatedEvents`, but only within a short bounded window.
*
* Deliberate divergence from client collapse: clients fold only *adjacent*
* lifecycle rows, so a superseded update separated from its completion by an
Expand Down Expand Up @@ -542,6 +542,55 @@ function dropSupersededToolUpdatedActivities(
});
}

/**
* Retain only the latest in-flight update for each tool call in a live batch.
* A later completion also supersedes preceding updates for the same call. All
* survivors stay in sequence order so client-side sequence dedup remains safe.
*/
export function coalesceLiveToolUpdatedEvents(
events: ReadonlyArray<OrchestrationEvent>,
): ReadonlyArray<OrchestrationEvent> {
const seenUpdates = new Set<string>();
const seenCompletions = new Set<string>();
const survivors: Array<OrchestrationEvent> = [];

for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index]!;
if (event.type !== "thread.activity-appended") {
survivors.push(event);
continue;
}

const activity = event.payload.activity;
if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") {
survivors.push(event);
continue;
}

const identity = toolLifecycleIdentity(activity);
if (!identity) {
survivors.push(event);
continue;
}

const key = `${activity.turnId ?? ""}\u0000${identity}`;
if (activity.kind === "tool.completed") {
seenCompletions.add(key);
survivors.push(event);
continue;
}

if (seenUpdates.has(key) || seenCompletions.has(key)) {
continue;
}
seenUpdates.add(key);
survivors.push(event);
}

survivors.reverse();
return survivors;
}

export function projectThreadDetailSnapshot(
snapshot: OrchestrationThreadDetailSnapshot,
): OrchestrationThreadDetailSnapshot {
Expand Down
239 changes: 239 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ExternalLauncherCommandNotFoundError,
OrchestrationThreadDetailSnapshot,
type OrchestrationThreadStreamItem,
type OrchestrationThreadActivity,
type OrchestrationThreadShell,
TerminalNotRunningError,
type OrchestrationCommand,
Expand All @@ -29,6 +30,7 @@ import {
ProviderInstanceId,
ResolvedKeybindingRule,
ThreadId,
TurnId,
WS_METHODS,
WsRpcGroup,
EditorId,
Expand Down Expand Up @@ -193,6 +195,44 @@ const defaultModelSelection = {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5-codex",
} as const;

const makeLiveToolActivityEvent = (
sequence: number,
kind: "tool.updated" | "tool.completed" = "tool.updated",
options: {
readonly toolCallId?: string;
readonly title?: string;
readonly path?: string;
} = {},
): Extract<OrchestrationEvent, { type: "thread.activity-appended" }> => {
const { toolCallId = "call-edit", title = "Editing app.ts", path = "src/app.ts" } = options;
const activity: OrchestrationThreadActivity = {
id: EventId.make(`activity-${sequence}`),
tone: "tool",
kind,
summary: title,
payload: {
itemType: "file_change",
title,
data: { toolCallId, path },
},
turnId: TurnId.make("turn-edit"),
createdAt: "2026-01-01T00:00:01.000Z",
};
return {
sequence,
eventId: EventId.make(`event-tool-${sequence}`),
aggregateKind: "thread",
aggregateId: defaultThreadId,
occurredAt: "2026-01-01T00:00:01.000Z",
commandId: null,
causationEventId: null,
correlationId: null,
metadata: {},
type: "thread.activity-appended",
payload: { threadId: defaultThreadId, activity },
};
};
const testEnvironmentDescriptor = {
environmentId: EnvironmentId.make("environment-test"),
label: "Test environment",
Expand Down Expand Up @@ -6421,6 +6461,205 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
}).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive),
);

it.effect("coalesces buffered live tool updates to the latest state", () =>
Effect.gen(function* () {
const thread = makeDefaultOrchestrationReadModel().threads[0]!;
const liveEvents = yield* PubSub.unbounded<OrchestrationEvent>();

yield* buildAppUnderTest({
layers: {
orchestrationEngine: {
streamDomainEvents: Stream.fromPubSub(liveEvents),
},
projectionSnapshotQuery: {
getThreadDetailSnapshot: () =>
Effect.gen(function* () {
yield* Effect.sleep("25 millis");
yield* PubSub.publishAll(liveEvents, [
makeLiveToolActivityEvent(2),
makeLiveToolActivityEvent(3),
makeLiveToolActivityEvent(4),
]);
return Option.some({ snapshotSequence: 1, thread });
}),
},
},
});

const wsUrl = yield* getWsServerUrl("/ws");
const items = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[ORCHESTRATION_WS_METHODS.subscribeThread]({
threadId: defaultThreadId,
}).pipe(Stream.take(2), Stream.runCollect),
),
).pipe(Effect.timeout("2 seconds"));

assert.equal(items[0]?.kind, "snapshot");
assert.equal(items[1]?.kind, "event");
assert.equal(items[1]?.kind === "event" ? items[1].event.sequence : null, 4);
}).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive),
);

it.effect("flushes more than one tool chunk before the synchronization marker", () =>
Effect.gen(function* () {
const thread = makeDefaultOrchestrationReadModel().threads[0]!;
const liveEvents = yield* PubSub.unbounded<OrchestrationEvent>();

yield* buildAppUnderTest({
layers: {
orchestrationEngine: {
streamDomainEvents: Stream.fromPubSub(liveEvents),
},
projectionSnapshotQuery: {
getThreadDetailSnapshot: () =>
Effect.gen(function* () {
yield* Effect.sleep("25 millis");
yield* PubSub.publishAll(liveEvents, [
...Array.from({ length: 512 }, (_, index) =>
makeLiveToolActivityEvent(index + 2),
),
makeLiveToolActivityEvent(514, "tool.updated", {
toolCallId: "call-read",
title: "Reading server.test.ts",
path: "apps/server/src/server.test.ts",
}),
]);
return Option.some({ snapshotSequence: 1, thread });
}),
},
},
});

const wsUrl = yield* getWsServerUrl("/ws");
const items = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[ORCHESTRATION_WS_METHODS.subscribeThread]({
threadId: defaultThreadId,
requestCompletionMarker: true,
}).pipe(Stream.take(4), Stream.runCollect),
),
).pipe(Effect.timeout("2 seconds"));

assert.equal(items[0]?.kind, "snapshot");
assert.deepEqual(
items.slice(1, 3).map((item) => {
assert.equal(item?.kind, "event");
if (item?.kind !== "event" || item.event.type !== "thread.activity-appended") {
return null;
}
return {
sequence: item.event.sequence,
summary: item.event.payload.activity.summary,
payload: item.event.payload.activity.payload,
};
}),
[
{
sequence: 513,
summary: "Editing app.ts",
payload: {
itemType: "file_change",
title: "Editing app.ts",
data: {
files: [{ path: "src/app.ts" }],
toolCallId: "call-edit",
},
},
},
{
sequence: 514,
summary: "Reading server.test.ts",
payload: {
itemType: "file_change",
title: "Reading server.test.ts",
data: {
files: [{ path: "apps/server/src/server.test.ts" }],
toolCallId: "call-read",
},
},
},
],
);
assert.deepEqual(items[3], { kind: "synchronized" });
}).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive),
);

it.effect("preserves an interleaved message while coalescing a tool lifecycle", () =>
Effect.gen(function* () {
const thread = makeDefaultOrchestrationReadModel().threads[0]!;
const liveEvents = yield* PubSub.unbounded<OrchestrationEvent>();
const messageEvent = {
sequence: 3,
eventId: EventId.make("event-interleaved-message"),
aggregateKind: "thread",
aggregateId: defaultThreadId,
occurredAt: "2026-01-01T00:00:02.000Z",
commandId: null,
causationEventId: null,
correlationId: null,
metadata: {},
type: "thread.message-sent",
payload: {
threadId: defaultThreadId,
messageId: MessageId.make("message-interleaved"),
role: "assistant",
text: "Still working",
turnId: TurnId.make("turn-edit"),
streaming: false,
createdAt: "2026-01-01T00:00:02.000Z",
updatedAt: "2026-01-01T00:00:02.000Z",
},
} satisfies Extract<OrchestrationEvent, { type: "thread.message-sent" }>;

yield* buildAppUnderTest({
layers: {
orchestrationEngine: {
streamDomainEvents: Stream.fromPubSub(liveEvents),
},
projectionSnapshotQuery: {
getThreadDetailSnapshot: () =>
Effect.gen(function* () {
yield* Effect.sleep("25 millis");
yield* PubSub.publishAll(liveEvents, [
makeLiveToolActivityEvent(2),
messageEvent,
makeLiveToolActivityEvent(4, "tool.completed"),
]);
return Option.some({ snapshotSequence: 1, thread });
}),
},
},
});

const wsUrl = yield* getWsServerUrl("/ws");
const items = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[ORCHESTRATION_WS_METHODS.subscribeThread]({
threadId: defaultThreadId,
}).pipe(Stream.take(3), Stream.runCollect),
),
).pipe(Effect.timeout("2 seconds"));

assert.equal(items[0]?.kind, "snapshot");
assert.deepEqual(
items
.slice(1)
.map((item) => (item.kind === "event" ? [item.event.sequence, item.event.type] : null)),
[
[3, "thread.message-sent"],
[4, "thread.activity-appended"],
],
);
assert.equal(
items[2]?.kind === "event" && items[2].event.type === "thread.activity-appended"
? items[2].event.payload.activity.kind
: null,
"tool.completed",
);
}).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive),
);

it.effect("subscribeThread sends a fresh snapshot instead of replaying a large gap", () =>
Effect.gen(function* () {
let readEventsCalls = 0;
Expand Down
Loading
Loading