Skip to content

Commit 3a2df65

Browse files
V48 Gate 3 (implementation-only): Hover last call chain on failed pipeline rows
Show error plus last phase→agent→step→failsafe→generation lines when hovering FAILED/CANCELLED/interrupted status pills. History supports ?tail=N for a lightweight event window used by the preview.
1 parent 9d02ddb commit 3a2df65

9 files changed

Lines changed: 628 additions & 32 deletions

packages/api/src/routes/executions.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ export async function postExecutionHistoryRoute(request: Request) {
631631
}
632632

633633
export async function getExecutionHistoryRunRoute(
634-
_request: Request,
634+
request: Request,
635635
params: { runId?: string | null | undefined },
636636
) {
637637
const userId = await requireExecutionRouteUserId();
@@ -644,6 +644,19 @@ export async function getExecutionHistoryRunRoute(
644644
return createJsonResponse({ error: 'Missing runId parameter' }, 400);
645645
}
646646

647+
// `?tail=N` — last N events only (for failure hover previews). Full history
648+
// when omitted (paginated past the PostgREST 1000-row default).
649+
let tail: number | null = null;
650+
try {
651+
const raw = new URL(request.url).searchParams.get('tail');
652+
if (raw) {
653+
const n = Number.parseInt(raw, 10);
654+
if (Number.isFinite(n) && n > 0) tail = Math.min(n, 50);
655+
}
656+
} catch {
657+
tail = null;
658+
}
659+
647660
const { data: run, error: runError } = await supabaseAdmin
648661
.from('executions')
649662
.select(
@@ -663,21 +676,18 @@ export async function getExecutionHistoryRunRoute(
663676
return createJsonResponse({ error: 'Execution not found or access denied' }, 404);
664677
}
665678

666-
// PostgREST/Supabase defaults to max 1000 rows. Deposit synthesis runs produce
667-
// multi-thousand event streams (QA: refresh dropped the second half of a
668-
// ~30m run and halved the clock because only the first 1000 events loaded).
669-
// Page through the full event history in order.
670-
const EVENT_PAGE_SIZE = 1000;
671679
const events: ExecutionEventRow[] = [];
672-
let eventOffset = 0;
673-
for (;;) {
680+
let eventsTruncated = false;
681+
682+
if (tail !== null) {
683+
// Newest-first page, then reverse so clients get chronological order.
674684
const { data: page, error: eventsError } = await supabaseAdmin
675685
.from('execution_events')
676686
.select('id, run_id, event_type, event_data, created_at, agent_name, phase')
677687
.eq('run_id', runId)
678-
.order('created_at', { ascending: true })
679-
.order('id', { ascending: true })
680-
.range(eventOffset, eventOffset + EVENT_PAGE_SIZE - 1);
688+
.order('created_at', { ascending: false })
689+
.order('id', { ascending: false })
690+
.range(0, tail - 1);
681691

682692
if (eventsError) {
683693
return createJsonResponse(
@@ -686,25 +696,56 @@ export async function getExecutionHistoryRunRoute(
686696
);
687697
}
688698
const batch = Array.isArray(page) ? (page as ExecutionEventRow[]) : [];
689-
events.push(...batch);
690-
if (batch.length < EVENT_PAGE_SIZE) break;
691-
eventOffset += EVENT_PAGE_SIZE;
692-
// Hard ceiling so a runaway table cannot unbounded-read the API process.
693-
if (eventOffset >= 50_000) break;
699+
events.push(...batch.reverse());
700+
} else {
701+
// PostgREST/Supabase defaults to max 1000 rows. Deposit synthesis runs produce
702+
// multi-thousand event streams (QA: refresh dropped the second half of a
703+
// ~30m run and halved the clock because only the first 1000 events loaded).
704+
// Page through the full event history in order.
705+
const EVENT_PAGE_SIZE = 1000;
706+
let eventOffset = 0;
707+
for (;;) {
708+
const { data: page, error: eventsError } = await supabaseAdmin
709+
.from('execution_events')
710+
.select('id, run_id, event_type, event_data, created_at, agent_name, phase')
711+
.eq('run_id', runId)
712+
.order('created_at', { ascending: true })
713+
.order('id', { ascending: true })
714+
.range(eventOffset, eventOffset + EVENT_PAGE_SIZE - 1);
715+
716+
if (eventsError) {
717+
return createJsonResponse(
718+
{ error: toErrorMessage(eventsError, 'Failed to fetch execution event history') },
719+
500,
720+
);
721+
}
722+
const batch = Array.isArray(page) ? (page as ExecutionEventRow[]) : [];
723+
events.push(...batch);
724+
if (batch.length < EVENT_PAGE_SIZE) break;
725+
eventOffset += EVENT_PAGE_SIZE;
726+
// Hard ceiling so a runaway table cannot unbounded-read the API process.
727+
if (eventOffset >= 50_000) {
728+
eventsTruncated = true;
729+
break;
730+
}
731+
}
694732
}
695733

696734
const normalizedRun = normalizeExecutionHistoryRow(run);
697-
const terminalJournal = await fetchTerminalJournalReadback(run.id, normalizedRun);
735+
// Skip heavy journal join for lightweight tail previews (table hover).
736+
const terminalJournal =
737+
tail !== null ? null : await fetchTerminalJournalReadback(run.id, normalizedRun);
698738

699739
return createJsonResponse({
700740
run: {
701741
...normalizedRun,
702-
terminal_journal: terminalJournal,
742+
...(terminalJournal ? { terminal_journal: terminalJournal } : {}),
703743
},
704744
events: events.map(normalizeExecutionEventRow),
705745
eventCount: events.length,
706-
eventsTruncated: eventOffset >= 50_000,
707-
terminal_journal: terminalJournal,
746+
eventsTruncated,
747+
tail,
748+
...(terminalJournal ? { terminal_journal: terminalJournal } : {}),
708749
});
709750
}
710751

uapi/app/terminal/terminal-transactions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface TerminalTransactionRecord {
2626
isOwnTransaction: boolean;
2727
transactionLens: Exclude<TerminalTransactionLens, 'all'>;
2828
searchableText: string;
29+
/** Source-safe failure text for failed/cancelled hover previews. */
30+
errorMessage?: string | null;
2931
}
3032

3133
export interface TerminalTransactionFilters {
@@ -65,6 +67,11 @@ export function normalizeTerminalTransactions(runs: WorkspaceRun[]): TerminalTra
6567
const summary =
6668
normalizeWhitespace(run.summary) || 'Inspect this Bitcode execution from the central Bitcode Terminal detail surface.';
6769
const status = normalizeStatus(run.status);
70+
const errorMessage =
71+
normalizeWhitespace(run.errorMessage) ||
72+
(status === 'failed' || status === 'cancelled' || status === 'interrupted'
73+
? normalizeWhitespace(run.summary)
74+
: null);
6875
const proofStatus = normalizeWhitespace(run.proofStatus) || agenticExecution.proofStatus;
6976
const closureFocus = normalizeWhitespace(run.closureFocus) || agenticExecution.closureFocus;
7077
const transactionLens = run.transactionLens || agenticExecution.lens;
@@ -76,6 +83,7 @@ export function normalizeTerminalTransactions(runs: WorkspaceRun[]): TerminalTra
7683
type,
7784
typeLabel,
7885
status,
86+
errorMessage,
7987
repository,
8088
branch,
8189
participant,
@@ -105,6 +113,7 @@ export function normalizeTerminalTransactions(runs: WorkspaceRun[]): TerminalTra
105113
isOwnTransaction: Boolean(run.isOwnTransaction),
106114
transactionLens,
107115
searchableText,
116+
errorMessage,
108117
};
109118
});
110119
}

uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { formatAgenticExecutionLabel } from '@bitcode/api/src/executions/agentic
77
import BitcodeInlineExplainer from './BitcodeInlineExplainer';
88
import { BITCODE_TRANSACTION_COLUMN_EXPLAINERS } from './bitcode-transaction-explainers';
99
import type { TransactionRecord } from './bitcode-transaction-types';
10+
import { TransactionStatusHoverBadge } from './TransactionStatusHoverBadge';
1011

1112
function formatTimestamp(value: string) {
1213
try {
@@ -26,12 +27,6 @@ function formatTypeLabel(value: string, label?: string) {
2627
return label || formatAgenticExecutionLabel(value);
2728
}
2829

29-
function statusTone(status: string) {
30-
if (status === 'completed') return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200';
31-
if (status === 'error' || status === 'failed') return 'border-red-500/30 bg-red-500/10 text-red-200';
32-
return 'border-amber-500/30 bg-amber-500/10 text-amber-100';
33-
}
34-
3530
interface BitcodeTransactionsDataTableProps {
3631
records: TransactionRecord[];
3732
selectedTransactionId: string | null;
@@ -142,11 +137,12 @@ export default function BitcodeTransactionsDataTable({
142137
</span>
143138
</td>
144139
<td className="px-3 py-3 align-top">
145-
<span
146-
className={` border px-2.5 py-1 text-[0.68rem] uppercase tracking-[0.18em] ${statusTone(record.status)}`}
147-
>
148-
{record.status}
149-
</span>
140+
<TransactionStatusHoverBadge
141+
runId={record.id}
142+
status={record.status}
143+
errorMessage={record.errorMessage}
144+
summary={record.summary}
145+
/>
150146
</td>
151147
<td className="px-3 py-3 align-top text-sm text-neutral-200">
152148
<p>{record.participant}</p>

0 commit comments

Comments
 (0)