Skip to content
Merged
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
6 changes: 4 additions & 2 deletions packages/api/src/postgres/trace-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ export class TraceRepository {
LEFT JOIN app.run_trace_projections p ON p.tenant_id=r.tenant_id AND p.run_id=r.id
WHERE r.tenant_id=$1 AND r.id=$2
), bound AS (SELECT revision,($5::text IS NOT NULL AND revision<>$5) reset FROM meta)
SELECT revision,reset,coalesce((SELECT jsonb_agg(to_jsonb(e) ORDER BY e.sequence) FROM (
SELECT revision,reset,coalesce((SELECT jsonb_agg(to_jsonb(e) ORDER BY CASE WHEN $6::boolean OR $7::bigint IS NOT NULL THEN -e.sequence ELSE e.sequence END) FROM (
SELECT sequence,id,parent_id,event_type,role,title,preview_text,display_data,occurred_at
FROM app.run_trace_events WHERE tenant_id=$1 AND run_id=$2
AND (CASE WHEN $6::boolean OR $7::bigint IS NOT NULL THEN sequence < COALESCE($7::bigint, (SELECT max(sequence) + 1 FROM app.run_trace_events WHERE tenant_id=$1 AND run_id=$2)) ELSE sequence > CASE WHEN reset THEN 0 ELSE $3 END END)
AND (CASE WHEN $6::boolean THEN sequence <= COALESCE((SELECT max(sequence) FROM app.run_trace_events WHERE tenant_id=$1 AND run_id=$2), 0)
WHEN $7::bigint IS NOT NULL THEN sequence < $7::bigint
ELSE sequence > CASE WHEN reset THEN 0 ELSE $3 END END)
ORDER BY CASE WHEN $6::boolean OR $7::bigint IS NOT NULL THEN sequence END DESC, CASE WHEN NOT ($6::boolean OR $7::bigint IS NOT NULL) THEN sequence END ASC LIMIT $4
) e),'[]'::jsonb) events FROM bound`, [this.tenantId, runId, after, limit + 1, expectedRevision ?? null, recent, before ?? null]);
const row = result.rows[0];
Expand Down
13 changes: 13 additions & 0 deletions packages/api/test/run-reads.postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ describe.skipIf(!url)("lightweight status and revision-bound traces", () => {
expect(await new TraceRepository(f.db, f.otherTenant).revisionPage(f.runId, 0, 2)).toBeNull();
expect((await new ApiService(f.env, f.auth).getRunStatus(f.runId)).trace_revision).toBe(reset.revision);
});
it("returns real recent and before pages without dropping or repeating boundaries", async () => {
const traces = new TraceRepository(f.db, f.tenantId);
await traces.replace(f.runId, Array.from({ length: 162 }, (_, i) => ({ id: `page-${i + 1}`, sequence: i + 1, type: "assistant_message" as const, title: `Event ${i + 1}`, preview: "Text", display: {} })));

const recent = (await traces.revisionPage(f.runId, 0, 100, undefined, undefined, true))!;
expect(recent.items.map(event => event.sequence)).toEqual(Array.from({ length: 100 }, (_, i) => i + 63));
expect(recent.previousCursor).toBe(63);

const older = (await traces.revisionPage(f.runId, 0, 100, recent.revision, recent.previousCursor!))!;
expect(older.items.map(event => event.sequence)).toEqual(Array.from({ length: 62 }, (_, i) => i + 1));
expect(older.previousCursor).toBeNull();
expect(new Set([...recent.items, ...older.items].map(event => event.sequence)).size).toBe(162);
});
it("distinguishes terminal execution from finalization and accepts legacy done filters", async () => {
await f.db.pool.query("UPDATE app.job_runs SET state='succeeded' WHERE tenant_id=$1 AND id=$2", [f.tenantId, f.runId]);
await f.db.pool.query("UPDATE app.runs SET state='succeeded',artifact_state='collecting',vm_cleanup_complete=false WHERE tenant_id=$1 AND id=$2", [f.tenantId, f.runId]);
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/features/runs/continuous-trace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function ContinuousTrace({ runId, revision, active, onReset }: { runId: s
if (!element) return;
const bottom = element.scrollHeight - element.scrollTop - element.clientHeight <= 48;
followRef.current = bottom;
if (element.scrollTop <= 8 && trace.hasNextPage && !trace.isFetching && !historyLoadingRef.current) void loadHistory(3);
};
const loadHistory = async (pageCount = 1) => {
const element = scrollRef.current;
Expand Down
9 changes: 6 additions & 3 deletions packages/app/test/runs.browser.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ test("run trace uses pull-to-load history, preserves the anchor, and never crash
const { context, page } = await setup(); const requests = [];
const event = (sequence, type = "assistant_message") => ({ id: `trace-${sequence}`, sequence, type, title: `Trace ${sequence}`, preview: `Trace ${sequence}`, display: {} });
const recent = [...Array.from({ length: 14 }, (_, index) => event(index + 11)), event(25, "tool_call"), event(26, "tool_result")];
const olderPages = [Array.from({ length: 10 }, (_, index) => event(index + 1)), Array.from({ length: 10 }, (_, index) => event(index + 27)), Array.from({ length: 10 }, (_, index) => event(index + 37))];
const olderPages = [Array.from({ length: 10 }, (_, index) => event(index + 1)), Array.from({ length: 10 }, (_, index) => event(index + 27)), Array.from({ length: 10 }, (_, index) => event(index + 37)), Array.from({ length: 10 }, (_, index) => event(index + 47)), Array.from({ length: 10 }, (_, index) => event(index + 57)), Array.from({ length: 10 }, (_, index) => event(index + 67))];
await page.route(`**/api/v1/runs/${runId}/status`, route => route.fulfill({ json: { id: runId, job_id: jobId, job_name: "Job", run_name: "Run", state: "succeeded", finalizing: false, trace_revision: "rev", artifact_state: "stored", created_at: time, updated_at: time, started_at: time, destination_url: null } }));
await page.route(`**/api/v1/runs/${runId}/trace-pages?**`, route => {
const url = new URL(route.request().url()); requests.push(url);
Expand All @@ -145,12 +145,15 @@ test("run trace uses pull-to-load history, preserves the anchor, and never crash
const initial = await scroller.evaluate(element => ({ top: element.scrollTop, height: element.scrollHeight, client: element.clientHeight }));
assert.ok(initial.height > initial.client && initial.top > 0, "initial trace should be bottom-aligned inside its own scroller");
const before = await scroller.evaluate(element => element.scrollHeight);
await scroller.evaluate(element => { element.scrollTop = 0; element.dispatchEvent(new Event("scroll", { bubbles: true })); });
await page.waitForFunction(() => document.querySelectorAll("[data-run-trace] article").length >= 44);
assert.equal(requests.filter(url => url.searchParams.has("before")).length, 3, "desktop top scroll should load history");
await scroller.evaluate(element => { element.scrollTop = 0; const event = new Event("touchstart", { bubbles: true }); Object.defineProperty(event, "touches", { value: [{ clientY: 100 }] }); element.dispatchEvent(event); });
await scroller.evaluate(element => { const event = new Event("touchmove", { bubbles: true }); Object.defineProperty(event, "touches", { value: [{ clientY: 180 }] }); element.dispatchEvent(event); });
await scroller.evaluate(element => { const event = new Event("touchend", { bubbles: true }); Object.defineProperty(event, "touches", { value: [] }); element.dispatchEvent(event); });
await page.waitForFunction(() => document.querySelectorAll("[data-run-trace] article").length >= 44);
await page.waitForFunction(() => document.querySelectorAll("[data-run-trace] article").length >= 74);
const after = await scroller.evaluate(element => ({ top: element.scrollTop, height: element.scrollHeight }));
assert.equal(requests.filter(url => url.searchParams.has("before")).length, 3, "a pull should load several history pages");
assert.equal(requests.filter(url => url.searchParams.has("before")).length, 6, "a pull should load several history pages");
assert.ok(after.top > 0 && after.height > before, "prepending history should preserve the visible anchor");
await page.setViewportSize({ width: 900, height: 700 }); const shortHeight = await scroller.evaluate(element => element.clientHeight);
await page.setViewportSize({ width: 900, height: 900 }); const tallHeight = await scroller.evaluate(element => element.clientHeight);
Expand Down
Loading