Skip to content

Commit d9c8f06

Browse files
V48 Gate 3 (implementation-only): Full event history reload and run clock
Page execution_events past the PostgREST 1000-row default, map executions.error into summary/errorMessage, and prefer started_at/ completed_at/duration_ms for the deposit run clock on refresh.
1 parent 488a978 commit d9c8f06

6 files changed

Lines changed: 92 additions & 13 deletions

File tree

packages/api/src/routes/executions.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -355,19 +355,38 @@ function buildRepoSnapshot(row: ExecutionHistoryRow) {
355355
};
356356
}
357357

358+
/**
359+
* Extract a human-readable failure message from executions.error JSON.
360+
* Failed/interrupted rows often have no output.summary — without this the
361+
* deposit UI falls through to the generic "Run failed." banner (QA: c7b84ad5).
362+
*/
363+
function readErrorMessage(error: unknown): string | null {
364+
if (typeof error === 'string' && error.trim()) return error.trim();
365+
if (!error || typeof error !== 'object' || Array.isArray(error)) return null;
366+
const record = error as JsonRecord;
367+
const message = asString(record.message) || asString(record.error) || asString(record.reason);
368+
return message;
369+
}
370+
358371
function buildSummary(row: ExecutionHistoryRow) {
359372
const output = readOutputRecord(row);
360373
const context = readContextRecord(row);
361374
const assetPackCompletion = readAssetPackCompletion(row);
362375
const assetPackSynthesisArtifacts = buildAssetPackSynthesisArtifacts(row);
363376
const writtenAssets = buildWrittenAssets(row);
377+
const status = String(row.status || '').toLowerCase();
378+
const terminalFailure =
379+
status === 'failed' || status === 'interrupted' || status === 'cancelled'
380+
? readErrorMessage(row.error)
381+
: null;
364382

365383
return (
366384
asString(output?.summary) ||
367385
asString(assetPackCompletion?.summary) ||
368386
asString(assetPackSynthesisArtifacts?.summary) ||
369387
asString(writtenAssets?.summary) ||
370388
asString(context?.summary) ||
389+
terminalFailure ||
371390
null
372391
);
373392
}
@@ -484,6 +503,9 @@ export function normalizeExecutionHistoryRow(row: ExecutionHistoryRow) {
484503
asset_pack_completion: buildNormalizedAssetPackCompletion(row),
485504
ledger_settlement: buildLedgerSettlement(row),
486505
error: row.error ?? null,
506+
// Wall-clock for run timers on refresh (not derived from truncated event windows).
507+
duration_ms: row.duration_ms ?? null,
508+
total_tokens: row.total_tokens ?? null,
487509
};
488510
}
489511

@@ -641,17 +663,34 @@ export async function getExecutionHistoryRunRoute(
641663
return createJsonResponse({ error: 'Execution not found or access denied' }, 404);
642664
}
643665

644-
const { data: events, error: eventsError } = await supabaseAdmin
645-
.from('execution_events')
646-
.select('id, run_id, event_type, event_data, created_at, agent_name, phase')
647-
.eq('run_id', runId)
648-
.order('created_at', { ascending: true });
649-
650-
if (eventsError) {
651-
return createJsonResponse(
652-
{ error: toErrorMessage(eventsError, 'Failed to fetch execution event history') },
653-
500,
654-
);
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;
671+
const events: ExecutionEventRow[] = [];
672+
let eventOffset = 0;
673+
for (;;) {
674+
const { data: page, error: eventsError } = await supabaseAdmin
675+
.from('execution_events')
676+
.select('id, run_id, event_type, event_data, created_at, agent_name, phase')
677+
.eq('run_id', runId)
678+
.order('created_at', { ascending: true })
679+
.order('id', { ascending: true })
680+
.range(eventOffset, eventOffset + EVENT_PAGE_SIZE - 1);
681+
682+
if (eventsError) {
683+
return createJsonResponse(
684+
{ error: toErrorMessage(eventsError, 'Failed to fetch execution event history') },
685+
500,
686+
);
687+
}
688+
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;
655694
}
656695

657696
const normalizedRun = normalizeExecutionHistoryRow(run);
@@ -662,7 +701,9 @@ export async function getExecutionHistoryRunRoute(
662701
...normalizedRun,
663702
terminal_journal: terminalJournal,
664703
},
665-
events: (events || []).map(normalizeExecutionEventRow),
704+
events: events.map(normalizeExecutionEventRow),
705+
eventCount: events.length,
706+
eventsTruncated: eventOffset >= 50_000,
666707
terminal_journal: terminalJournal,
667708
});
668709
}

uapi/app/terminal/terminal-activity-history.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,17 @@ export function buildTerminalExternalInterfacingDraft(
741741
};
742742
}
743743

744+
function readExecutionErrorMessage(error: unknown): string | null {
745+
if (typeof error === 'string' && error.trim()) return error.trim();
746+
if (!error || typeof error !== 'object' || Array.isArray(error)) return null;
747+
const record = error as Record<string, unknown>;
748+
for (const key of ['message', 'error', 'reason'] as const) {
749+
const value = record[key];
750+
if (typeof value === 'string' && value.trim()) return value.trim();
751+
}
752+
return null;
753+
}
754+
744755
export function mapExecutionHistoryRunToWorkspaceRun(run: PipelineExecution): WorkspaceRun {
745756
const agenticExecution =
746757
run.agentic_execution ||
@@ -754,6 +765,14 @@ export function mapExecutionHistoryRunToWorkspaceRun(run: PipelineExecution): Wo
754765
const value = context?.[key];
755766
return typeof value === 'string' && value.trim() ? value.trim() : null;
756767
};
768+
const errorMessage = readExecutionErrorMessage((run as { error?: unknown }).error);
769+
const statusLower = String(run.status || '').toLowerCase();
770+
const failureSummary =
771+
statusLower === 'failed' ||
772+
statusLower === 'interrupted' ||
773+
statusLower === 'cancelled'
774+
? errorMessage
775+
: null;
757776

758777
return {
759778
id: run.id,
@@ -762,13 +781,15 @@ export function mapExecutionHistoryRunToWorkspaceRun(run: PipelineExecution): Wo
762781
type: agenticExecution.canonicalType,
763782
agentic_execution: agenticExecution,
764783
sourceModel: 'execution-history',
784+
errorMessage,
765785
summary:
766786
run.summary ||
767787
run.asset_pack_completion?.summary ||
768788
run.asset_pack_completion?.assetPackSynthesisArtifacts?.summary ||
769789
run.asset_pack_completion?.writtenAssets?.summary ||
770790
run.asset_pack_completion?.shippables?.summary ||
771791
run.asset_pack_completion?.deliveryMechanism?.summary ||
792+
failureSummary ||
772793
null,
773794
repository:
774795
repoSnapshot

uapi/app/terminal/terminal-run-data.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { TerminalRunDetailSnapshot } from './terminal-transaction-detail-sn
44

55
export type WorkspaceRun = Pick<PipelineExecution, 'id' | 'created_at' | 'type' | 'agentic_execution' | 'status'> & {
66
summary?: string | null;
7+
/** Source-safe failure message from executions.error (failed/interrupted). */
8+
errorMessage?: string | null;
79
repository?: string | null;
810
branch?: string | null;
911
sourceCommit?: string | null;

uapi/hooks/usePipelineExecution.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
33
interface PipelineExecution {
44
id: string;
55
created_at: string;
6+
started_at?: string | null;
7+
completed_at?: string | null;
8+
duration_ms?: number | null;
9+
status?: string | null;
610
items: any[];
711
context: any;
12+
error?: unknown;
813
}
914

1015
interface ExecutionEvent {

uapi/tests/api/executionsHistoryRunRoute.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ describe('GET /api/executions/history/[runId]', () => {
125125
const eventsBuilder: any = {
126126
select: jest.fn().mockReturnThis(),
127127
eq: jest.fn().mockReturnThis(),
128-
order: jest.fn().mockResolvedValue({
128+
order: jest.fn().mockReturnThis(),
129+
// Paginated history: .range() ends the chain (PostgREST 1000-row pages).
130+
range: jest.fn().mockResolvedValue({
129131
data: [
130132
{
131133
id: '1',
@@ -224,9 +226,14 @@ describe('GET /api/executions/history/[runId]', () => {
224226
expect(res.status).toBe(200);
225227

226228
const json = await res.json();
229+
expect(eventsBuilder.range).toHaveBeenCalledWith(0, 999);
230+
expect(json.eventCount).toBe(2);
231+
expect(json.events).toHaveLength(2);
227232
expect(json.run).toEqual(
228233
expect.objectContaining({
229234
id: 'run-1',
235+
started_at: '2026-04-22T11:58:00.000Z',
236+
completed_at: '2026-04-22T12:04:00.000Z',
230237
summary: 'Persisted closure posture.',
231238
guide: 'refresh proof families',
232239
repo_snapshot: {

uapi/types/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ export interface PipelineExecution {
165165
/** Optional markdown summary stored by backend */
166166
summary?: string | null;
167167

168+
/** Terminal failure payload from executions.error (source-safe message). */
169+
error?: { message?: string | null } | string | null;
170+
168171
/** Execution metrics (time, tokens, measured $BTD, BTC fee posture, latency) */
169172
processing_stats?: {
170173
time: string;

0 commit comments

Comments
 (0)