fix: preserve accurate history boundaries in run views

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 07:40:28 -05:00
1 parent b197da6e60
commit 8859f37fa2
7 files changed
+68 -29

No files matched your search

@@ -484,7 +484,7 @@ describe("agent live run routes", () => {
beforeSeq: undefined,
limit: 200,
});
expect(forward.body).toEqual([{ seq: 11, eventType: "stdout", payload: null, historyAfter: false }]);
expect(forward.body).toEqual([{ seq: 11, eventType: "stdout", payload: null, historyBefore: false, historyAfter: false }]);
for (const query of ["afterSeq=-1", "afterSeq=1.5", "afterSeq=tail&beforeSeq=-1", "limit=1001"]) {
const invalid = await requestApp(app, (url) =>
@@ -48,5 +48,7 @@ describe("bounded heartbeat run event pages", () => {
});
expect(page).toHaveLength(1);
expect(page[0].seq).toBe(1);
expect(page[0].historyBefore).toBe(false);
expect(page[0].historyAfter).toBe(false);
});
});
@@ -33,7 +33,7 @@ export function boundHeartbeatRunEventPage<T extends { seq: number }>(input: {
const after = baseAfter || (byteCut && input.direction === "forward");
return events.map((event, index) => ({
...event,
...(before && index === 0 ? { historyBefore: true as const } : {}),
...(index === 0 ? { historyBefore: before } : {}),
...(index === events.length - 1 ? { historyAfter: after } : {}),
}));
};
+1
View File
@@ -64,6 +64,7 @@ test("long native history opens at the latest activity and older events remain b
await page.getByRole("button", { name: /Earlier activity/ }).last().click();
await page.getByRole("link", { name: "View run", exact: true }).last().click();
await expect(page).toHaveURL(new RegExp(`/runs/${run.id}$`));
await expect(page.getByTestId("run-event-history-notice")).toBeVisible();
await page.getByRole("button", { name: /Inspect/ }).first().click();
await page.getByRole("tab", { name: "Pipeline", exact: true }).click();
await expect(page.getByText("Recent activity · 1,000 events")).toBeVisible();
@@ -39,7 +39,8 @@ it.each([LogViewer, ProductionLogViewer])("starts from the recent event tail and
message: `event-${seq}`,
payload: null,
createdAt: new Date("2026-09-10T12:00:00Z"),
...(seq === 1_004 ? { historyAfter: true } : {}),
...(seq === 0 ? { historyBefore: true } : {}),
...(seq === 1_004 ? { historyAfter: false } : {}),
}));
events.mockReturnValue(eventRows as never[]);
const run = { id: "run-1", companyId: "company-1", agentId: "agent-1", status: "succeeded", logRef: null } as HeartbeatRun;
@@ -64,6 +65,39 @@ it.each([LogViewer, ProductionLogViewer])("starts from the recent event tail and
}
});
it.each([LogViewer, ProductionLogViewer])("shows the omission notice from the tail page's historyBefore marker (%#)", async (Viewer) => {
const eventRow = {
id: 10,
companyId: "company-1",
runId: "run-1",
agentId: "agent-1",
seq: 10,
eventType: "event",
stream: "system" as const,
level: "info" as const,
color: null,
message: "recent event",
payload: null,
createdAt: new Date("2026-09-10T12:00:00Z"),
historyBefore: true as const,
historyAfter: false,
};
events.mockReturnValue([eventRow] as never[]);
const run = { id: "run-1", companyId: "company-1", agentId: "agent-1", status: "succeeded", logRef: null } as HeartbeatRun;
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render(<Viewer run={run} adapterType="codex_local" onOpenInspector={vi.fn()} />));
expect(events).toHaveBeenCalledWith("run-1", "tail", 200);
expect(container.textContent).toContain("Events (1)");
expect(container.querySelector('[data-testid="run-event-history-notice"]')?.textContent).toContain("browse older activity");
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it.each([LogViewer, ProductionLogViewer])("retains legacy history and reads only the next offset on visibility recovery (%#)", async (Viewer) => {
const visibility = vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
const row = (seq: number, chunk: string) => JSON.stringify({ seq, ts: `2026-09-10T12:00:0${seq}Z`, stream: "stdout", chunk }) + "\n";
+14 -13
View File
@@ -3738,8 +3738,8 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
export function LogViewer({ run, adapterType, onOpenInspector }: { run: HeartbeatRun; adapterType: string; onOpenInspector?: () => void }) {
const { visible } = usePageVisibility();
const [events, setEvents] = useState<HeartbeatRunEvent[]>([]);
const [eventHistoryOmitted, setEventHistoryOmitted] = useState(false);
const [eventState, setEventState] = useState({ events: [] as HeartbeatRunEvent[], historyOmitted: false });
const { events, historyOmitted: eventHistoryOmitted } = eventState;
const [logLines, setLogLines] = useState<RunLogChunk[]>([]);
const [loading, setLoading] = useState(true);
const [logLoading, setLogLoading] = useState(!!run.logRef);
@@ -3824,23 +3824,20 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
});
useEffect(() => {
setEvents([]);
setEventHistoryOmitted(false);
setEventState({ events: [], historyOmitted: false });
}, [run.id]);
useEffect(() => {
if (initialEvents) {
const retained = retainEventTail(initialEvents);
setEvents(retained.events);
setEventHistoryOmitted(retained.collapsed || initialEvents.at(-1)?.historyAfter === true);
setEventState({
events: retained.events,
historyOmitted: retained.collapsed || initialEvents[0]?.historyBefore === true,
});
setLoading(false);
}
}, [initialEvents]);
useEffect(() => {
if (retainEventTail(events).collapsed) setEventHistoryOmitted(true);
}, [events]);
const getScrollContainer = useCallback((): ScrollContainer => {
if (scrollContainerRef.current) return scrollContainerRef.current;
const container = findScrollContainer(logEndRef.current);
@@ -4004,7 +4001,10 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100);
if (cancelled) return;
if (newEvents.length > 0) {
setEvents((prev) => retainEventTail(mergeRunEvents(prev, newEvents)).events);
setEventState((prev) => {
const retained = retainEventTail(mergeRunEvents(prev.events, newEvents));
return { events: retained.events, historyOmitted: prev.historyOmitted || retained.collapsed };
});
}
} catch {
// ignore polling errors
@@ -4145,8 +4145,9 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
createdAt: new Date(event.createdAt),
};
setEvents((prev) => {
return retainEventTail(mergeRunEvents(prev, [liveEvent])).events;
setEventState((prev) => {
const retained = retainEventTail(mergeRunEvents(prev.events, [liveEvent]));
return { events: retained.events, historyOmitted: prev.historyOmitted || retained.collapsed };
});
};
+14 -13
View File
@@ -3983,8 +3983,8 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
export function LogViewer({ run, adapterType, onOpenInspector }: { run: HeartbeatRun; adapterType: string; onOpenInspector?: () => void }) {
const { visible } = usePageVisibility();
const [events, setEvents] = useState<HeartbeatRunEvent[]>([]);
const [eventHistoryOmitted, setEventHistoryOmitted] = useState(false);
const [eventState, setEventState] = useState({ events: [] as HeartbeatRunEvent[], historyOmitted: false });
const { events, historyOmitted: eventHistoryOmitted } = eventState;
const [logLines, setLogLines] = useState<RunLogChunk[]>([]);
const [loading, setLoading] = useState(true);
const [logLoading, setLogLoading] = useState(!!run.logRef);
@@ -4069,23 +4069,20 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
});
useEffect(() => {
setEvents([]);
setEventHistoryOmitted(false);
setEventState({ events: [], historyOmitted: false });
}, [run.id]);
useEffect(() => {
if (initialEvents) {
const retained = retainEventTail(initialEvents);
setEvents(retained.events);
setEventHistoryOmitted(retained.collapsed || initialEvents.at(-1)?.historyAfter === true);
setEventState({
events: retained.events,
historyOmitted: retained.collapsed || initialEvents[0]?.historyBefore === true,
});
setLoading(false);
}
}, [initialEvents]);
useEffect(() => {
if (retainEventTail(events).collapsed) setEventHistoryOmitted(true);
}, [events]);
const getScrollContainer = useCallback((): ScrollContainer => {
if (scrollContainerRef.current) return scrollContainerRef.current;
const container = findScrollContainer(logEndRef.current);
@@ -4249,7 +4246,10 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100);
if (cancelled) return;
if (newEvents.length > 0) {
setEvents((prev) => retainEventTail(mergeRunEvents(prev, newEvents)).events);
setEventState((prev) => {
const retained = retainEventTail(mergeRunEvents(prev.events, newEvents));
return { events: retained.events, historyOmitted: prev.historyOmitted || retained.collapsed };
});
}
} catch {
// ignore polling errors
@@ -4390,8 +4390,9 @@ export function LogViewer({ run, adapterType, onOpenInspector }: { run: Heartbea
createdAt: new Date(event.createdAt),
};
setEvents((prev) => {
return retainEventTail(mergeRunEvents(prev, [liveEvent])).events;
setEventState((prev) => {
const retained = retainEventTail(mergeRunEvents(prev.events, [liveEvent]));
return { events: retained.events, historyOmitted: prev.historyOmitted || retained.collapsed };
});
};