@@ -288,6 +288,105 @@ describe('ExecutionLogger', () => {
288288 expect(emitExecutionCompletedEvent).not.toHaveBeenCalled()
289289 })
290290
291+ const EMPTY_STATE = {
292+ blockStates: {},
293+ executedBlocks: [],
294+ blockLogs: [],
295+ decisions: { router: {}, condition: {} },
296+ completedLoops: [],
297+ activeExecutionPath: [],
298+ }
299+ const RUN_PROVENANCE = { version: 1, complete: true, entries: [] }
300+
301+ /**
302+ * Drives a real completion and returns the `execution_data` actually written.
303+ * `redactedState` stands in for the PII pass, which either hands back a
304+ * redacted state or none at all.
305+ */
306+ async function completeAndReadWrite(params: {
307+ executionState?: SerializableExecutionState
308+ redactedState?: SerializableExecutionState
309+ }) {
310+ const startedAt = new Date('2026-08-11T00:00:00.000Z')
311+ queueTableRows(workflowExecutionLogs, [
312+ {
313+ id: 'log-1',
314+ workflowId: 'workflow-1',
315+ workspaceId: 'workspace-1',
316+ executionId: 'execution-1',
317+ stateSnapshotId: 'snapshot-1',
318+ level: 'info',
319+ status: 'running',
320+ trigger: 'api',
321+ startedAt,
322+ endedAt: null,
323+ totalDurationMs: null,
324+ executionData: {},
325+ createdAt: startedAt,
326+ },
327+ ])
328+ dbChainMockFns.returning.mockResolvedValueOnce([
329+ { id: 'log-1', executionData: {}, startedAt, createdAt: startedAt },
330+ ])
331+ vi.spyOn(logger as any, 'applyPiiRedaction').mockImplementation(
332+ async (_workspaceId: unknown, payload: any) =>
333+ Object.hasOwn(params, 'redactedState')
334+ ? { ...payload, executionState: params.redactedState }
335+ : payload
336+ )
337+ vi.spyOn(logger as any, 'recordExecutionUsage').mockResolvedValue(0)
338+
339+ await logger.completeWorkflowExecution({
340+ executionId: 'execution-1',
341+ endedAt: '2026-08-11T00:00:02.000Z',
342+ totalDurationMs: 2000,
343+ costSummary: {
344+ totalCost: 0,
345+ totalInputCost: 0,
346+ totalOutputCost: 0,
347+ totalTokens: 0,
348+ totalPromptTokens: 0,
349+ totalCompletionTokens: 0,
350+ baseExecutionCharge: 0,
351+ models: {},
352+ },
353+ finalOutput: { completed: true },
354+ traceSpans: [],
355+ ...(params.executionState ? { executionState: params.executionState } : {}),
356+ })
357+
358+ return dbChainMockFns.set.mock.calls
359+ .map(([values]: [any]) => values?.executionData)
360+ .find((data: any) => data && typeof data === 'object')
361+ }
362+
363+ /**
364+ * The display projection rebuilds its redaction registry from this key.
365+ * Compaction drops `executionState`, so the run provenance has to reach the
366+ * row independently of it or truncated runs render as an empty trace.
367+ */
368+ test.each([
369+ ['redaction preserves the state', EMPTY_STATE],
370+ ['redaction drops the state entirely', undefined],
371+ ])('lifts run provenance onto the top-level key when %s', async (_case, redactedState) => {
372+ const written = await completeAndReadWrite({
373+ executionState: {
374+ ...EMPTY_STATE,
375+ resolvedSecretTraceProvenance: RUN_PROVENANCE,
376+ } as unknown as SerializableExecutionState,
377+ redactedState: redactedState as SerializableExecutionState | undefined,
378+ })
379+
380+ expect(written?.resolvedSecretTraceProvenance).toEqual(RUN_PROVENANCE)
381+ })
382+
383+ test('omits the provenance key when the run carried none', async () => {
384+ const written = await completeAndReadWrite({})
385+
386+ expect(written).toBeDefined()
387+ expect(written).not.toHaveProperty('resolvedSecretTraceProvenance')
388+ })
389+
291390 test('preserves correlation and diagnostics when execution completes', () => {
292391 const loggerInstance = new ExecutionLogger() as any
293392
@@ -628,6 +727,79 @@ describe('ExecutionLogger', () => {
628727 expect(compacted.traceSpans?.[0]?.toolCalls?.[0]).not.toHaveProperty('output')
629728 expect(compacted.traceSpans?.[0]?.toolCalls?.[0]).not.toHaveProperty('error')
630729 })
730+
731+ const PROVENANCE = { version: 1, complete: true, entries: [] } as const
732+
733+ function buildSpans(spanCount: number, ioBytes: number) {
734+ const payload = 'x'.repeat(ioBytes)
735+ return Array.from({ length: spanCount }, (_unused, index) => ({
736+ id: `span-${index}`,
737+ name: `Block ${index}`,
738+ type: 'function',
739+ duration: 1,
740+ startTime: '2025-01-01T00:00:00.000Z',
741+ endTime: '2025-01-01T00:00:01.000Z',
742+ status: 'success' as const,
743+ output: { data: payload },
744+ }))
745+ }
746+
747+ function compactWithProvenance(traceSpans: unknown[], finalOutput: unknown) {
748+ const loggerInstance = new ExecutionLogger() as any
749+ return loggerInstance.compactExecutionDataForStorage(
750+ {
751+ secretProjectionVersion: SECRET_PROJECTION_VERSION,
752+ resolvedSecretTraceProvenance: PROVENANCE,
753+ hasTraceSpans: true,
754+ traceSpanCount: traceSpans.length,
755+ finalOutput,
756+ executionState: {
757+ blockStates: {},
758+ executedBlocks: [],
759+ blockLogs: [],
760+ decisions: { router: {}, condition: {} },
761+ completedLoops: [],
762+ activeExecutionPath: [],
763+ resolvedSecretTraceProvenance: PROVENANCE,
764+ },
765+ traceSpans,
766+ },
767+ 'execution-provenance'
768+ )
769+ }
770+
771+ test('preserves run provenance through the summarized compaction tier', () => {
772+ // One oversized value: summarization alone brings the row under the cap.
773+ const compacted = compactWithProvenance(buildSpans(1, 4 * 1024 * 1024), {
774+ data: 'x'.repeat(4 * 1024 * 1024),
775+ })
776+
777+ expect(compacted.executionDataTruncated).toBe(true)
778+ expect(compacted.executionDataTruncationReason).toContain('were summarized')
779+ expect(compacted.executionState).toBeUndefined()
780+ expect(compacted.resolvedSecretTraceProvenance).toEqual(PROVENANCE)
781+ })
782+
783+ test('drops run provenance from the metadata-only tier, which stores no spans', () => {
784+ // That tier keeps no traceSpans, so provenance there buys nothing and
785+ // would put an unbounded value in the last-resort size floor.
786+ const compacted = compactWithProvenance(buildSpans(20_000, 8), {})
787+
788+ expect(compacted.executionDataTruncationReason).toContain('only execution metadata')
789+ expect(compacted.traceSpans).toBeUndefined()
790+ expect(compacted.resolvedSecretTraceProvenance).toBeUndefined()
791+ })
792+
793+ test('preserves run provenance through the minimal compaction tier', () => {
794+ // Many spans whose IO each sits under MAX_TRACE_IO_BYTES survive
795+ // summarization, so only the IO-stripping minimal tier fits the cap.
796+ const compacted = compactWithProvenance(buildSpans(1200, 4 * 1024), {})
797+
798+ expect(compacted.executionDataTruncated).toBe(true)
799+ expect(compacted.executionDataTruncationReason).toContain('details were omitted')
800+ expect(compacted.executionState).toBeUndefined()
801+ expect(compacted.resolvedSecretTraceProvenance).toEqual(PROVENANCE)
802+ })
631803 })
632804
633805 describe('file extraction', () => {
0 commit comments