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
131 changes: 131 additions & 0 deletions apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type OrchestrationV2RunAttempt,
type OrchestrationV2TurnItem,
} from "@t3tools/contracts";
import { mergeOlderHistoryIntoProjection } from "@t3tools/client-runtime/state/threads";
import type { ThreadRuntimeSummary } from "@t3tools/client-runtime/state/shell";
import { deriveMessagesTimelineRows } from "./components/chat/MessagesTimeline.logic";
import * as DateTime from "effect/DateTime";
Expand Down Expand Up @@ -1405,6 +1406,136 @@ describe("work-log failure policy (#7999/#7893)", () => {
});

describe("incremental v2 timeline entries", () => {
it("reuses history presentation after a prepend while refreshing sources and previews", () => {
const fixture = makeStreamingTimelineFixture("Partial");
const attempt: OrchestrationV2RunAttempt = {
id: RunAttemptId.make("live-attempt"),
runId: fixture.runId,
attemptOrdinal: 1,
rootNodeId: NodeId.make("live-root"),
providerInstanceId: ProviderInstanceId.make("codex"),
providerThreadId: ProviderThreadId.make("live-provider-thread"),
providerTurnId: null,
reason: "initial",
status: "running",
startedAt: DateTime.makeUnsafe(fixture.time(5)),
completedAt: null,
};
const attachment = {
type: "image" as const,
id: "live-image",
name: "image.png",
mimeType: "image/png",
sizeBytes: 42,
};
const visibleTurnItems = fixture.visibleTurnItems.slice(3).map((row, position) => ({
...row,
position,
item: {
...row.item,
nodeId: attempt.rootNodeId,
...(row.item.type === "user_message" ? { attachments: [attachment] } : {}),
},
}));
const base = makeThreadProjectionFixture();
const projection = {
...base,
thread: { ...base.thread, id: fixture.threadId },
visibleTurnItems,
turnItems: visibleTurnItems.map((row) => row.item),
};
const input = {
visibleTurnItems,
optimisticMessages: [],
attachmentUrlById: new Map([[attachment.id, "https://server.test/image"]]),
attempts: [attempt],
};
const previous = deriveTimelineEntriesFromVisibleTurnItemsWithState(input);
const merged = mergeOlderHistoryIntoProjection(
projection,
fixture.visibleTurnItems.slice(0, 3),
);
const nextInput = {
...input,
visibleTurnItems: merged.visibleTurnItems,
attachmentUrlById: new Map([
...input.attachmentUrlById,
["older-image", "https://server.test/older-image"],
]),
};
const next = deriveTimelineEntriesFromVisibleTurnItemsWithState(nextInput, previous);
expect(next.entries).toEqual(deriveTimelineEntriesFromVisibleTurnItems(nextInput));
const rowsFor = (entries: TimelineEntry[]) =>
deriveMessagesTimelineRows({
timelineEntries: entries,
latestRun: null,
isWorking: false,
activeTurnStartedAt: null,
turnDiffSummaries: [],
supportsConversationRollback: false,
});
const previousRows = rowsFor(previous.entries);
const nextRows = rowsFor(next.entries);
for (const entry of next.entries) {
const source =
entry.kind === "message" || entry.kind === "event"
? entry.projectedItem
: entry.kind === "work"
? entry.entry.projectedItem
: undefined;
expect(source).toBe(
merged.visibleTurnItems.find((row) => row.sourceItemId === source?.sourceItemId),
);
}
const completedAttempt = {
...attempt,
status: "completed" as const,
completedAt: DateTime.makeUnsafe(fixture.time(8)),
};
const completedInput = { ...nextInput, attempts: [completedAttempt] };
const completed = deriveTimelineEntriesFromVisibleTurnItemsWithState(completedInput, next);
expect(completed.entries).toEqual(deriveTimelineEntriesFromVisibleTurnItems(completedInput));
const removedInput = { ...completedInput, attempts: [] };
const removed = deriveTimelineEntriesFromVisibleTurnItemsWithState(removedInput, completed);
expect(removed.entries).toEqual(deriveTimelineEntriesFromVisibleTurnItems(removedInput));
for (const entry of next.entries) {
if (entry.kind !== "message" || entry.message.runId !== fixture.runId) continue;
const refreshed = completed.entries.find((candidate) => candidate.id === entry.id);
const cleared = removed.entries.find((candidate) => candidate.id === entry.id);
expect(refreshed?.kind === "message" && refreshed.message).toBe(entry.message);
expect(refreshed?.attempt).toBe(completedAttempt);
expect(cleared?.kind === "message" && cleared.message).toBe(entry.message);
expect(cleared).not.toHaveProperty("attempt");
expect(entry.attempt).toBe(attempt);
expect(refreshed?.attempt?.status).toBe("completed");
}
const renewedInput = {
...nextInput,
attachmentUrlById: new Map([[attachment.id, "https://renewed.test/image"]]),
};
const renewed = deriveTimelineEntriesFromVisibleTurnItemsWithState(renewedInput, next);
expect(renewed.entries).toEqual(deriveTimelineEntriesFromVisibleTurnItems(renewedInput));
expect(
rowsFor(renewed.entries).find(
(row) => row.kind === "message" && row.message.id === "live-user",
),
).toMatchObject({ message: { attachments: [{ previewUrl: "https://renewed.test/image" }] } });
expect(previous.entries[0]).toMatchObject({
message: { attachments: [{ previewUrl: "https://server.test/image" }] },
projectedItem: { position: 0 },
});
for (const entry of previous.entries) {
if (entry.kind !== "work") continue;
const retained = next.entries.find((nextEntry) => nextEntry.id === entry.id);
expect(retained?.kind === "work" && retained.entry.toolData).toBe(entry.entry.toolData);
}
for (const row of previousRows) {
if (row.kind !== "message") continue;
const retained = nextRows.find((nextRow) => nextRow.id === row.id);
expect(retained?.kind === "message" && retained.message).toBe(row.message);
}
});

it("retains history and attachment previews through decoded streaming updates", () => {
const fixture = makeStreamingTimelineFixture("Partial");
const attachment = {
Expand Down
73 changes: 54 additions & 19 deletions apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,14 @@ export interface TimelineEntriesProjection {

export function deriveTimelineEntriesFromVisibleTurnItems(
input: TimelineEntriesInput,
previous: TimelineEntriesProjection | null = null,
): TimelineEntry[] {
const committedMessageIds = new Set<string>();
const entries: TimelineEntry[] = [];
const previousById = new Map(previous?.entries.map((entry) => [entry.id, entry]));
const previousRows = new Map(previous?.input.visibleTurnItems.map((row) => [row.item, row]));
const sameAttachmentUrls =
previous !== null && shallow(input.attachmentUrlById, previous.input.attachmentUrlById);
const attemptByRootNodeId = new Map(
(input.attempts ?? []).map((attempt) => [attempt.rootNodeId, attempt] as const),
);
Expand Down Expand Up @@ -617,8 +622,55 @@ export function deriveTimelineEntriesFromVisibleTurnItems(
if (turnItemIsWorkspacePreparation(item)) continue;
if (item.type === "todo_list" || item.type === "checkpoint") continue;
if (item.type === "user_message" && foldedAnswerMessageIds.has(item.messageId)) continue;
const createdAt = projectedItemCreatedAt(row);
const attempt = resolveAttempt(item);
const previousRow = previousRows.get(item);
const old = previousById.get(
item.type === "user_message" || item.type === "assistant_message" ? item.messageId : item.id,
);
if (
previous !== null &&
previousRow !== undefined &&
previousRow.visibility === row.visibility &&
previousRow.sourceThreadId === row.sourceThreadId &&
previousRow.sourceItemId === row.sourceItemId &&
old !== undefined &&
(old.kind === "message" || old.attempt === attempt) &&
(item.type !== "proposed_plan" || input.plans === previous.input.plans) &&
(sameAttachmentUrls ||
(item.type !== "user_message" && item.type !== "assistant_message") ||
(item.attachments ?? []).every(
(attachment) =>
input.attachmentUrlById?.get(attachment.id) ===
previous.input.attachmentUrlById?.get(attachment.id),
))
) {
// History prepends renumber wrappers while retaining immutable item payloads.
if (old.kind === "message") {
if (previousRow === row && old.attempt === attempt) {
entries.push(old);
} else {
const { attempt: _previousAttempt, ...rest } = old;
entries.push({
...rest,
projectedItem: row,
...(attempt === undefined ? {} : { attempt }),
});
}
committedMessageIds.add(old.message.id);
continue;
}
entries.push(
previousRow === row
? old
: old.kind === "work"
? { ...old, entry: { ...old.entry, projectedItem: row } }
: old.kind === "event"
? { ...old, projectedItem: row }
: old,
);
continue;
}
const createdAt = projectedItemCreatedAt(row);
const attemptMetadata = attempt === undefined ? {} : { attempt };
if (item.type === "notification") {
entries.push({
Expand Down Expand Up @@ -991,24 +1043,7 @@ export function deriveTimelineEntriesFromVisibleTurnItemsWithState(
): TimelineEntriesProjection {
const reused = previous === null ? null : reuseTimelineEntries(input, previous);
if (reused !== null) return { input, entries: reused };
const entries = deriveTimelineEntriesFromVisibleTurnItems(input);
if (previous === null || !shallow(input.attachmentUrlById, previous.input.attachmentUrlById)) {
return { input, entries };
}
const previousMessages = new Map(
previous.entries.flatMap((entry) =>
entry.kind === "message" ? [[entry.id, entry] as const] : [],
),
);
return {
input,
entries: entries.map((entry) => {
if (entry.kind !== "message" || entry.projectedItem === undefined) return entry;
const before = previousMessages.get(entry.id);
if (before?.projectedItem !== entry.projectedItem) return entry;
return before.attempt === entry.attempt ? before : { ...entry, message: before.message };
}),
};
return { input, entries: deriveTimelineEntriesFromVisibleTurnItems(input, previous) };
}

export function deriveRevertTurnCountByUserMessageId(input: {
Expand Down
3 changes: 2 additions & 1 deletion packages/client-runtime/src/rpc/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ export const makeEnvironmentHttpApiGroupClient = <
>(
httpBaseUrl: string,
group: Group,
api = EnvironmentHttpApi,
) =>
Effect.flatMap(HttpClient.HttpClient, (httpClient) =>
HttpApiClient.group(EnvironmentHttpApi, {
HttpApiClient.group(api, {
httpClient,
group,
baseUrl: remoteApiBaseUrl(httpBaseUrl),
Expand Down
106 changes: 105 additions & 1 deletion packages/client-runtime/src/state/environmentHttpAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
OrchestrationV2ThreadDetailSnapshot,
OrchestrationV2ThreadBoundedSnapshot,
OrchestrationV2ThreadTranscript,
type OrchestrationV2ThreadHistoryPage,
OrchestrationV2ThreadHistoryPage,
} from "@t3tools/contracts";
import { vi } from "vite-plus/test";
import { RelayClientTracer } from "@t3tools/shared/relayTracing";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -47,6 +48,9 @@ import { v2Projection } from "./orchestrationV2TestFixtures.ts";
const encodeThreadSnapshot = Schema.encodeSync(OrchestrationV2ThreadDetailSnapshot);
const encodeBoundedSnapshot = Schema.encodeSync(OrchestrationV2ThreadBoundedSnapshot);
const encodeThreadTranscript = Schema.encodeSync(OrchestrationV2ThreadTranscript);
const decodeThreadHistory = Schema.decodeUnknownEffect(
Schema.toCodecJson(OrchestrationV2ThreadHistoryPage),
);

const TARGET = new RelayConnectionTarget({
environmentId: EnvironmentId.make("environment-1"),
Expand Down Expand Up @@ -265,6 +269,106 @@ const LOADERS: ReadonlyArray<{
];

describe("authenticated environment HTTP requests", () => {
it.live("yields during history validation and only returns a complete compatible page", () =>
Effect.gen(function* () {
const items = Array.from({ length: 129 }, (_, position) => ({
position,
visibility: "local",
sourceThreadId: THREAD.projection.thread.id,
sourceItemId: `history-${position}`,
item: {
id: `history-${position}`,
threadId: THREAD.projection.thread.id,
runId: null,
nodeId: null,
providerThreadId: null,
providerTurnId: null,
nativeItemRef: null,
parentItemId: null,
ordinal: position,
status: "completed",
title: null,
startedAt: "2026-06-20T00:00:00.000Z",
completedAt: "2026-06-20T00:00:00.000Z",
updatedAt: "2026-06-20T00:00:00.000Z",
type: position === 64 ? "future_item" : "command_execution",
input: "echo é",
},
}));
const response = {
...THREAD_HISTORY,
items,
};
const expected = yield* decodeThreadHistory(response);
const body = JSON.stringify(response);
const visited = new Set<number>();
const hostTask = Promise.withResolvers<number>();
const channel = new MessageChannel();
channel.port1.addEventListener("message", () => hostTask.resolve(visited.size), {
once: true,
});
channel.port1.start();
let elapsed = 0;
const clock = vi.spyOn(performance, "now").mockImplementation(() => elapsed);
const parse = JSON.parse;
const parser = vi.spyOn(JSON, "parse").mockImplementation((text, reviver) => {
const parsed: unknown = parse(text, reviver);
if (text !== body) return parsed;
const page = parsed as typeof response;
for (const row of page.items) {
const startedAt = row.item.startedAt;
Object.defineProperty(row.item, "startedAt", {
enumerable: true,
get() {
// Model expensive validation without burning CPU or depending on wall time.
elapsed += 1;
if (visited.size === 0) channel.port2.postMessage(null);
visited.add(row.position);
return startedAt;
},
});
}
return page;
});
try {
const harness = makeHarness((requestNumber) =>
requestNumber === 1
? new Response(body, { headers: { "content-type": "application/json" } })
: Response.json({
...response,
items: [
...items.slice(0, -1),
{
...items.at(-1)!,
item: {
...items.at(-1)!.item,
startedAt: "invalid-date",
},
},
],
}),
);
const load = fetchEnvironmentThreadHistoryPage({
...harness.input,
threadId: THREAD.projection.thread.id,
cursor: "older-page",
}).pipe(Effect.provide(harness.httpLayer));
const page = yield* load;
const progressAtHostTask = yield* Effect.promise(() => hostTask.promise);
expect(page).toEqual(expected);
expect(page.items).toHaveLength(128);
expect((yield* Effect.flip(load))._tag).toBe("RemoteEnvironmentAuthInvalidJsonError");
expect(progressAtHostTask).toBeGreaterThan(0);
expect(progressAtHostTask).toBeLessThan(visited.size);
} finally {
channel.port1.close();
channel.port2.close();
parser.mockRestore();
clock.mockRestore();
}
}),
);

it.effect.each(LOADERS)("rejects an invalid $name response", (loader) =>
Effect.gen(function* () {
const harness = makeHarness(() => Response.json({}));
Expand Down
Loading
Loading